fel kesz game backend
This commit is contained in:
@@ -10,6 +10,7 @@ import chatRouter from './routers/chatRouter';
|
||||
import contactRouter from './routers/contactRouter';
|
||||
import adminRouter from './routers/adminRouter';
|
||||
import deckImportExportRouter from './routers/deckImportExportRouter';
|
||||
import gameRouter from './routers/gameRouter';
|
||||
import { LoggingService, logStartup, logConnection, logError, logRequest } from '../Application/Services/Logger';
|
||||
import { WebSocketService } from '../Application/Services/WebSocketService';
|
||||
import { setupSwagger } from './swagger/swaggerUiSetup';
|
||||
@@ -131,6 +132,7 @@ app.use('/api/chats', chatRouter);
|
||||
app.use('/api/contacts', contactRouter);
|
||||
app.use('/api/admin', adminRouter);
|
||||
app.use('/api/deck-import-export', deckImportExportRouter);
|
||||
app.use('/api/games', gameRouter);
|
||||
|
||||
// Global error handler (must be after routes)
|
||||
app.use(loggingService.errorLoggingMiddleware());
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { JWTService } from '../../Application/Services/JWTService';
|
||||
import { UserState } from '../../Domain/User/UserAggregate';
|
||||
import { logAuth, logWarning } from '../../Application/Services/Logger';
|
||||
|
||||
interface AuthenticatedRequest extends Request {
|
||||
user?: {
|
||||
userId: string;
|
||||
authLevel: 0 | 1;
|
||||
userStatus: UserState;
|
||||
orgId: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional authentication middleware - extracts JWT data if present but doesn't require authentication
|
||||
* Used for endpoints that work for both authenticated and anonymous users
|
||||
*/
|
||||
export const optionalAuth = (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
|
||||
const jwtService = new JWTService();
|
||||
|
||||
try {
|
||||
// Try to extract token from Authorization header or cookies
|
||||
const authHeader = req.headers.authorization;
|
||||
const token = authHeader?.startsWith('Bearer ')
|
||||
? authHeader.substring(7)
|
||||
: req.cookies?.auth_token;
|
||||
|
||||
if (token) {
|
||||
// If token exists, try to verify it
|
||||
const payload = jwtService.verify(req);
|
||||
|
||||
if (payload) {
|
||||
req.user = {
|
||||
userId: payload.userId,
|
||||
authLevel: payload.authLevel,
|
||||
userStatus: payload.userStatus,
|
||||
orgId: payload.orgId || null
|
||||
};
|
||||
|
||||
logAuth('Optional auth - user authenticated', payload.userId, {
|
||||
authLevel: payload.authLevel,
|
||||
userStatus: payload.userStatus,
|
||||
orgId: payload.orgId
|
||||
});
|
||||
} else {
|
||||
logWarning('Optional auth - invalid token provided', {
|
||||
hasToken: true,
|
||||
tokenLength: token.length
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Continue regardless of authentication status
|
||||
next();
|
||||
|
||||
} catch (error) {
|
||||
// Log the error but continue without authentication
|
||||
logWarning('Optional auth - error processing token', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
hasAuthHeader: !!req.headers.authorization,
|
||||
hasCookie: !!req.cookies?.auth_token
|
||||
});
|
||||
|
||||
next();
|
||||
}
|
||||
};
|
||||
@@ -107,38 +107,6 @@ router.get('/users/page/:from/:to', adminRequired, async (req: Request, res: Res
|
||||
}
|
||||
});
|
||||
|
||||
// Get users by page (admin only) - RECOMMENDED
|
||||
router.get('/users/page/:from/:to', adminRequired, async (req: Request, res: Response) => {
|
||||
try {
|
||||
const from = parseInt(req.params.from);
|
||||
const to = parseInt(req.params.to);
|
||||
const includeDeleted = req.query.includeDeleted === 'true';
|
||||
|
||||
if (isNaN(from) || isNaN(to) || from < 0 || to < from) {
|
||||
return res.status(400).json({ error: 'Invalid page parameters. "from" and "to" must be valid numbers with to >= from >= 0' });
|
||||
}
|
||||
|
||||
logRequest('Admin get users by page endpoint accessed', req, res, { from, to, includeDeleted });
|
||||
|
||||
const result = includeDeleted
|
||||
? await container.userRepository.findByPageIncludingDeleted(from, to)
|
||||
: await container.userRepository.findByPage(from, to);
|
||||
|
||||
logRequest('Admin users page retrieved successfully', req, res, {
|
||||
from,
|
||||
to,
|
||||
count: result.users.length,
|
||||
total: result.totalCount,
|
||||
includeDeleted
|
||||
});
|
||||
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
logError('Admin get users by page endpoint error', error as Error, req, res);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get user by ID including soft-deleted ones
|
||||
router.get('/users/:userId',
|
||||
adminRequired,
|
||||
@@ -173,32 +141,32 @@ router.get('/users/:userId',
|
||||
});
|
||||
|
||||
// Search users including soft-deleted ones
|
||||
router.get('/users/search/:searchTerm',
|
||||
adminRequired,
|
||||
ValidationMiddleware.validateStringLength({ searchTerm: { min: 2, max: 100 } }),
|
||||
async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { searchTerm } = req.params;
|
||||
const includeDeleted = req.query.includeDeleted === 'true';
|
||||
// router.get('/users/search/:searchTerm',
|
||||
// adminRequired,
|
||||
// ValidationMiddleware.validateStringLength({ searchTerm: { min: 2, max: 100 } }),
|
||||
// async (req: Request, res: Response) => {
|
||||
// try {
|
||||
// const { searchTerm } = req.params;
|
||||
// const includeDeleted = req.query.includeDeleted === 'true';
|
||||
|
||||
logRequest('Admin search users endpoint accessed', req, res, { searchTerm, includeDeleted });
|
||||
// logRequest('Admin search users endpoint accessed', req, res, { searchTerm, includeDeleted });
|
||||
|
||||
const users = includeDeleted
|
||||
? await container.userRepository.searchIncludingDeleted(searchTerm)
|
||||
: await container.userRepository.search(searchTerm);
|
||||
// const users = includeDeleted
|
||||
// ? await container.userRepository.searchIncludingDeleted(searchTerm)
|
||||
// : await container.userRepository.search(searchTerm);
|
||||
|
||||
logRequest('Admin user search completed', req, res, {
|
||||
searchTerm,
|
||||
resultCount: Array.isArray(users) ? users.length : (users.totalCount || 0),
|
||||
includeDeleted
|
||||
});
|
||||
// logRequest('Admin user search completed', req, res, {
|
||||
// searchTerm,
|
||||
// resultCount: Array.isArray(users) ? users.length : (users.totalCount || 0),
|
||||
// includeDeleted
|
||||
// });
|
||||
|
||||
res.json(users);
|
||||
} catch (error) {
|
||||
logError('Admin search users endpoint error', error as Error, req, res);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
// res.json(users);
|
||||
// } catch (error) {
|
||||
// logError('Admin search users endpoint error', error as Error, req, res);
|
||||
// res.status(500).json({ error: 'Internal server error' });
|
||||
// }
|
||||
// });
|
||||
|
||||
// Update any user (admin only)
|
||||
router.patch('/users/:userId',
|
||||
@@ -390,6 +358,30 @@ router.get('/decks/search/:searchTerm', adminRequired, async (req: Request, res:
|
||||
}
|
||||
});
|
||||
|
||||
//modify deck (admin only)
|
||||
router.patch('/decks/:id', adminRequired, async (req: Request, res: Response) => {
|
||||
try {
|
||||
const deckId = req.params.id;
|
||||
const adminUserId = (req as any).user.userId;
|
||||
logRequest('Admin update deck endpoint accessed', req, res, { deckId, adminUserId, updateFields: Object.keys(req.body) });
|
||||
const result = await container.updateDeckCommandHandler.execute({ id: deckId, userstate: 1 , ...req.body});
|
||||
logRequest('Deck updated successfully by admin', req, res, { deckId, adminUserId });
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
logError('Admin update deck endpoint error', error as Error, req, res);
|
||||
if (error instanceof Error && error.message.includes('not found')) {
|
||||
return res.status(404).json({ error: `Deck not found` });
|
||||
}
|
||||
if (error instanceof Error && (error.message.includes('duplicate') || error.message.includes('unique constraint'))) {
|
||||
return res.status(409).json({ error: 'Deck with this name already exists' });
|
||||
}
|
||||
if (error instanceof Error && error.message.includes('validation')) {
|
||||
return res.status(400).json({ error: 'Invalid input data', details: error.message });
|
||||
}
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Hard delete deck (admin only)
|
||||
router.delete('/decks/:id/hard', adminRequired, async (req: Request, res: Response) => {
|
||||
try {
|
||||
|
||||
@@ -60,7 +60,7 @@ deckRouter.post('/', authRequired, async (req, res) => {
|
||||
try {
|
||||
const userId = (req as any).user.userId;
|
||||
logRequest('Create deck endpoint accessed', req, res, { name: req.body.name, userId });
|
||||
|
||||
req.body.userid = userId; // Set userId in request body
|
||||
const result = await container.createDeckCommandHandler.execute(req.body);
|
||||
|
||||
logRequest('Deck created successfully', req, res, { deckId: result.id, name: req.body.name, userId });
|
||||
@@ -140,7 +140,7 @@ deckRouter.get('/:id', authRequired, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
deckRouter.put('/:id', authRequired, async (req, res) => {
|
||||
deckRouter.patch('/:id', authRequired, async (req, res) => {
|
||||
try {
|
||||
const deckId = req.params.id;
|
||||
const userId = (req as any).user.userId;
|
||||
@@ -164,6 +164,10 @@ deckRouter.put('/:id', authRequired, async (req, res) => {
|
||||
if (error instanceof Error && error.message.includes('validation')) {
|
||||
return res.status(400).json({ error: 'Invalid input data', details: error.message });
|
||||
}
|
||||
|
||||
if (error instanceof Error && error.message.includes('admin')) {
|
||||
return res.status(403).json({ error: 'Forbidden: ' + error.message });
|
||||
}
|
||||
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -32,7 +32,7 @@ userRouter.post('/login',
|
||||
logAuth('User login successful', result.user.id, { username: result.user.username }, req, res);
|
||||
res.json(result);
|
||||
} else {
|
||||
return ErrorResponseService.sendUnauthorized(res, 'Invalid username or password');
|
||||
throw new Error(`Login failed: ${result}`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
@@ -48,6 +48,9 @@ userRouter.post('/login',
|
||||
if (error.message.includes('not verified')) {
|
||||
return ErrorResponseService.sendUnauthorized(res, 'Please verify your email address');
|
||||
}
|
||||
if (error.message.includes('restriction')) {
|
||||
return ErrorResponseService.sendUnauthorized(res, 'Please verify your email address');
|
||||
}
|
||||
if (error.message.includes('deactivated')) {
|
||||
return ErrorResponseService.sendUnauthorized(res, 'Account has been deactivated');
|
||||
}
|
||||
@@ -84,7 +87,8 @@ userRouter.post('/create',
|
||||
res.status(201).json(result);
|
||||
|
||||
} catch (error) {
|
||||
logError('Create user endpoint error', error as Error, req, res);
|
||||
// Don't log here since CreateUserCommandHandler already logs system errors
|
||||
// Only log validation/user input errors at router level
|
||||
|
||||
if (error instanceof Error) {
|
||||
if (error.message.includes('already exists')) {
|
||||
@@ -93,6 +97,10 @@ userRouter.post('/create',
|
||||
if (error.message.includes('validation')) {
|
||||
return ErrorResponseService.sendBadRequest(res, error.message);
|
||||
}
|
||||
// Log unexpected errors that weren't handled by the command handler
|
||||
if (!error.message.includes('Failed to create user')) {
|
||||
logError('Unexpected create user endpoint error', error as Error, req, res);
|
||||
}
|
||||
}
|
||||
|
||||
return ErrorResponseService.sendInternalServerError(res);
|
||||
@@ -165,4 +173,29 @@ userRouter.patch('/profile', authRequired, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
//Soft delete user (current user)
|
||||
userRouter.delete('/profile', authRequired, async (req, res) => {
|
||||
try {
|
||||
const userId = (req as any).user.userId;
|
||||
const result = await container.deleteUserCommandHandler.execute({ id: userId, soft: true });
|
||||
logRequest('User soft deleted successfully', req, res, { userId });
|
||||
res.json({ success: result });
|
||||
} catch (error) {
|
||||
logError('Soft delete user endpoint error', error as Error, req, res);
|
||||
return ErrorResponseService.sendInternalServerError(res);
|
||||
}
|
||||
});
|
||||
|
||||
//logout user (current user)
|
||||
userRouter.post('/logout', authRequired, async (req, res) => {
|
||||
try {
|
||||
const userId = (req as any).user.userId;
|
||||
await container.logoutCommandHandler.execute(userId, res, req);
|
||||
logRequest('User logged out successfully', req, res, { userId });
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
logError('Logout user endpoint error', error as Error, req, res);
|
||||
return ErrorResponseService.sendInternalServerError(res);
|
||||
}
|
||||
});
|
||||
export default userRouter;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import swaggerJSDoc from 'swagger-jsdoc';
|
||||
import path from 'path';
|
||||
|
||||
export const swaggerOptions = {
|
||||
definition: {
|
||||
@@ -18,9 +19,13 @@ export const swaggerOptions = {
|
||||
},
|
||||
servers: [
|
||||
{
|
||||
url: 'http://localhost:3000',
|
||||
url: 'http://localhost:3001',
|
||||
description: 'Local development server'
|
||||
},
|
||||
{
|
||||
url: 'http://localhost:3000',
|
||||
description: 'Local development server (alt)'
|
||||
},
|
||||
{
|
||||
url: 'https://api.serpentrace.com',
|
||||
description: 'Production server'
|
||||
@@ -61,11 +66,35 @@ export const swaggerOptions = {
|
||||
{
|
||||
name: 'Deck Import/Export',
|
||||
description: 'Import and export deck functionality'
|
||||
},
|
||||
{
|
||||
name: 'Games',
|
||||
description: 'Game management and gameplay'
|
||||
},
|
||||
{
|
||||
name: 'Admin - Users',
|
||||
description: 'Admin user management operations'
|
||||
},
|
||||
{
|
||||
name: 'Admin - Decks',
|
||||
description: 'Admin deck management operations'
|
||||
},
|
||||
{
|
||||
name: 'Admin - Organizations',
|
||||
description: 'Admin organization management operations'
|
||||
},
|
||||
{
|
||||
name: 'Admin - Chats',
|
||||
description: 'Admin chat management operations'
|
||||
},
|
||||
{
|
||||
name: 'Admin - Contacts',
|
||||
description: 'Admin contact management operations'
|
||||
}
|
||||
]
|
||||
},
|
||||
apis: [
|
||||
'./src/Api/swagger/swaggerDefinitions.ts'
|
||||
'./src/Api/swagger/swaggerDefinitionsFixed.ts'
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
/**
|
||||
* @swagger
|
||||
* components:
|
||||
* securitySchemes:
|
||||
* bearerAuth:
|
||||
* type: http
|
||||
* scheme: bearer
|
||||
* bearerFormat: JWT
|
||||
* schemas:
|
||||
* User:
|
||||
* type: object
|
||||
@@ -299,6 +294,34 @@
|
||||
* chatId:
|
||||
* type: string
|
||||
*
|
||||
* Game:
|
||||
* type: object
|
||||
* properties:
|
||||
* id:
|
||||
* type: string
|
||||
* format: uuid
|
||||
* gamecode:
|
||||
* type: string
|
||||
* maxplayers:
|
||||
* type: integer
|
||||
* logintype:
|
||||
* type: integer
|
||||
* gamedecks:
|
||||
* type: array
|
||||
* players:
|
||||
* type: array
|
||||
* items:
|
||||
* type: string
|
||||
* started:
|
||||
* type: boolean
|
||||
* finished:
|
||||
* type: boolean
|
||||
* state:
|
||||
* type: integer
|
||||
* createdate:
|
||||
* type: string
|
||||
* format: date-time
|
||||
*
|
||||
* Error:
|
||||
* type: object
|
||||
* properties:
|
||||
@@ -309,32 +332,35 @@
|
||||
* format: date-time
|
||||
* details:
|
||||
* type: string
|
||||
*
|
||||
* paths:
|
||||
* /api/users/login:
|
||||
* post:
|
||||
* tags: [Users]
|
||||
* summary: User login
|
||||
* description: Authenticate user and return JWT token
|
||||
* requestBody:
|
||||
* required: true
|
||||
*/
|
||||
/**
|
||||
* @swagger
|
||||
*
|
||||
* /api/users/login:
|
||||
* post:
|
||||
* tags: [Users]
|
||||
* summary: User login
|
||||
* description: Authenticate user and return JWT token
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/LoginRequest'
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Login successful
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/LoginRequest'
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Login successful
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/LoginResponse'
|
||||
* 401:
|
||||
* description: Invalid credentials
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
* $ref: '#/components/schemas/LoginResponse'
|
||||
* 401:
|
||||
* description: Invalid credentials
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
*
|
||||
*
|
||||
* /api/users/create:
|
||||
* post:
|
||||
@@ -1397,6 +1423,163 @@
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Contact'
|
||||
*
|
||||
* /api/games/start:
|
||||
* post:
|
||||
* summary: Start a new game
|
||||
* tags: [Games]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - deckids
|
||||
* - maxplayers
|
||||
* - logintype
|
||||
* properties:
|
||||
* deckids:
|
||||
* type: array
|
||||
* items:
|
||||
* type: string
|
||||
* description: Array of deck IDs (must include all 3 types LUCK, JOKER, QUESTION)
|
||||
* maxplayers:
|
||||
* type: integer
|
||||
* minimum: 2
|
||||
* maximum: 8
|
||||
* description: Maximum number of players allowed in the game
|
||||
* logintype:
|
||||
* type: integer
|
||||
* enum: [0, 1, 2]
|
||||
* description: How players can join (PUBLIC=0, PRIVATE=1, ORGANIZATION=2)
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Game started successfully
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* id:
|
||||
* type: string
|
||||
* description: Game UUID
|
||||
* gamecode:
|
||||
* type: string
|
||||
* description: 6-character game code for joining
|
||||
* maxplayers:
|
||||
* type: integer
|
||||
* logintype:
|
||||
* type: integer
|
||||
* gamedecks:
|
||||
* type: array
|
||||
* description: Shuffled game decks
|
||||
* players:
|
||||
* type: array
|
||||
* items:
|
||||
* type: string
|
||||
* started:
|
||||
* type: boolean
|
||||
* finished:
|
||||
* type: boolean
|
||||
* state:
|
||||
* type: integer
|
||||
* description: Game state (WAITING=0, ACTIVE=1, FINISHED=2, CANCELLED=3)
|
||||
* createdate:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* 400:
|
||||
* description: Invalid input parameters
|
||||
* 401:
|
||||
* description: Authentication required
|
||||
* 500:
|
||||
* description: Internal server error
|
||||
*
|
||||
* /api/games/join:
|
||||
* post:
|
||||
* summary: Join a game (automatically detects game type)
|
||||
* description: Join any game by providing the game code. The system automatically determines if authentication is required based on the game type.
|
||||
* tags: [Games]
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - gameCode
|
||||
* properties:
|
||||
* gameCode:
|
||||
* type: string
|
||||
* description: 6-character game code
|
||||
* example: "ABC123"
|
||||
* playerName:
|
||||
* type: string
|
||||
* description: Display name for the player (required for public games, optional for authenticated games)
|
||||
* example: "John Doe"
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Successfully joined the game
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Game'
|
||||
* 400:
|
||||
* description: Invalid input or missing required fields
|
||||
* 401:
|
||||
* description: Authentication required for this game type
|
||||
* 403:
|
||||
* description: Organization membership required
|
||||
* 404:
|
||||
* description: Game not found
|
||||
* 409:
|
||||
* description: Game is full or not accepting players
|
||||
* 500:
|
||||
* description: Internal server error
|
||||
*
|
||||
* /api/games/{gameId}/start:
|
||||
* post:
|
||||
* summary: Start gameplay for an existing game
|
||||
* description: Initialize gameplay by setting all player positions to 0 and assigning random turn order. This is separate from game creation.
|
||||
* tags: [Games]
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: gameId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: The ID of the game to start
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Game started successfully
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* message:
|
||||
* type: string
|
||||
* example: "Game started successfully"
|
||||
* gameId:
|
||||
* type: string
|
||||
* example: "game123"
|
||||
* playerCount:
|
||||
* type: number
|
||||
* example: 4
|
||||
* 400:
|
||||
* description: Invalid input or game cannot be started
|
||||
* 401:
|
||||
* description: Authentication required
|
||||
* 403:
|
||||
* description: Only game master can start the game
|
||||
* 404:
|
||||
* description: Game not found
|
||||
* 409:
|
||||
* description: Game already started or not ready to start
|
||||
* 500:
|
||||
* description: Internal server error
|
||||
*/
|
||||
|
||||
export {};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user