final changes

This commit is contained in:
2025-09-22 11:14:32 +02:00
parent cf157643d7
commit bf9ae5f01f
509 changed files with 920 additions and 64152 deletions
+20 -8
View File
@@ -10,12 +10,13 @@ import chatRouter from './routers/chatRouter';
import contactRouter from './routers/contactRouter';
import adminRouter from './routers/adminRouter';
import deckImportExportRouter from './routers/deckImportExportRouter';
<<<<<<< HEAD
import gameRouter from './routers/gameRouter';
=======
>>>>>>> origin/main
import { LoggingService, logStartup, logConnection, logError, logRequest } from '../Application/Services/Logger';
import { WebSocketService } from '../Application/Services/WebSocketService';
import { GameWebSocketService } from '../Application/Services/GameWebSocketService';
import { GameRepository } from '../Infrastructure/Repository/GameRepository';
import { UserRepository } from '../Infrastructure/Repository/UserRepository';
import { RedisService } from '../Application/Services/RedisService';
import { setupSwagger } from './swagger/swaggerUiSetup';
const app = express();
@@ -135,10 +136,7 @@ app.use('/api/chats', chatRouter);
app.use('/api/contacts', contactRouter);
app.use('/api/admin', adminRouter);
app.use('/api/deck-import-export', deckImportExportRouter);
<<<<<<< HEAD
app.use('/api/games', gameRouter);
=======
>>>>>>> origin/main
// Global error handler (must be after routes)
app.use(loggingService.errorLoggingMiddleware());
@@ -167,6 +165,7 @@ app.use((req: express.Request, res: express.Response) => {
// Initialize WebSocket service after database connection
let webSocketService: WebSocketService;
let gameWebSocketService: GameWebSocketService;
// Initialize database connection
AppDataSource.initialize()
@@ -183,6 +182,19 @@ AppDataSource.initialize()
logStartup('WebSocket service initialized', {
chatInactivityTimeout: process.env.CHAT_INACTIVITY_TIMEOUT_MINUTES || '30'
});
// Initialize Game WebSocket service for /game namespace
const gameRepository = new GameRepository();
const userRepository = new UserRepository();
const redisService = RedisService.getInstance();
gameWebSocketService = new GameWebSocketService(
webSocketService['io'], // Access the io property directly
gameRepository,
userRepository,
redisService
);
logStartup('Game WebSocket service initialized for /game namespace');
})
.catch((error) => {
const dbOptions = AppDataSource.options as any;
@@ -254,5 +266,5 @@ process.on('unhandledRejection', (reason, promise) => {
process.exit(1);
});
// Export WebSocket service for game integration
export { webSocketService };
// Export WebSocket services for game integration
export { webSocketService, gameWebSocketService };
@@ -107,41 +107,6 @@ router.get('/users/page/:from/:to', adminRequired, async (req: Request, res: Res
}
});
<<<<<<< HEAD
=======
// 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' });
}
});
>>>>>>> origin/main
// Get user by ID including soft-deleted ones
router.get('/users/:userId',
adminRequired,
@@ -176,7 +141,6 @@ router.get('/users/:userId',
});
// Search users including soft-deleted ones
<<<<<<< HEAD
// router.get('/users/search/:searchTerm',
// adminRequired,
// ValidationMiddleware.validateStringLength({ searchTerm: { min: 2, max: 100 } }),
@@ -203,34 +167,6 @@ router.get('/users/:userId',
// res.status(500).json({ error: 'Internal server error' });
// }
// });
=======
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 });
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
});
res.json(users);
} catch (error) {
logError('Admin search users endpoint error', error as Error, req, res);
res.status(500).json({ error: 'Internal server error' });
}
});
>>>>>>> origin/main
// Update any user (admin only)
router.patch('/users/:userId',
@@ -422,7 +358,6 @@ router.get('/decks/search/:searchTerm', adminRequired, async (req: Request, res:
}
});
<<<<<<< HEAD
//modify deck (admin only)
router.patch('/decks/:id', adminRequired, async (req: Request, res: Response) => {
try {
@@ -447,8 +382,6 @@ router.patch('/decks/:id', adminRequired, async (req: Request, res: Response) =>
}
});
=======
>>>>>>> origin/main
// Hard delete deck (admin only)
router.delete('/decks/:id/hard', adminRequired, async (req: Request, res: Response) => {
try {
@@ -5,9 +5,53 @@ import { ErrorResponseService } from '../../Application/Services/ErrorResponseSe
import { ValidationMiddleware } from '../../Application/Services/ValidationMiddleware';
import { GeneralSearchService } from '../../Application/Search/Generalsearch';
import { logRequest, logError, logWarning } from '../../Application/Services/Logger';
import { Type, CType } from '../../Domain/Deck/DeckAggregate';
const deckRouter = Router();
/**
* Helper function to convert string enum values to integer enum values
*/
function convertEnumValues(data: any): any {
const converted = { ...data };
// Convert Type enum
if (converted.type && typeof converted.type === 'string') {
switch (converted.type.toUpperCase()) {
case 'LUCK':
converted.type = Type.LUCK;
break;
case 'JOKER':
converted.type = Type.JOKER;
break;
case 'QUESTION':
converted.type = Type.QUESTION;
break;
default:
throw new Error('Invalid deck type. Must be LUCK, JOKER, or QUESTION');
}
}
// Convert CType enum
if (converted.ctype && typeof converted.ctype === 'string') {
switch (converted.ctype.toUpperCase()) {
case 'PUBLIC':
converted.ctype = CType.PUBLIC;
break;
case 'PRIVATE':
converted.ctype = CType.PRIVATE;
break;
case 'ORGANIZATION':
converted.ctype = CType.ORGANIZATION;
break;
default:
throw new Error('Invalid deck ctype. Must be PUBLIC, PRIVATE, or ORGANIZATION');
}
}
return converted;
}
// Create search service that isn't in the container yet
const searchService = new GeneralSearchService(container.userRepository, container.organizationRepository, container.deckRepository);
@@ -60,18 +104,25 @@ 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 });
<<<<<<< HEAD
req.body.userid = userId; // Set userId in request body
=======
>>>>>>> origin/main
const result = await container.createDeckCommandHandler.execute(req.body);
// Convert string enum values to integers
const command = convertEnumValues({
...req.body,
userid: userId
});
const result = await container.createDeckCommandHandler.execute(command);
logRequest('Deck created successfully', req, res, { deckId: result.id, name: req.body.name, userId });
res.json(result);
} catch (error) {
logError('Create deck endpoint error', error as Error, req, res);
// Handle enum validation errors
if (error instanceof Error && error.message.includes('Invalid deck')) {
return res.status(400).json({ error: error.message });
}
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' });
}
@@ -144,23 +195,27 @@ deckRouter.get('/:id', authRequired, async (req, res) => {
}
});
<<<<<<< HEAD
deckRouter.patch('/:id', authRequired, async (req, res) => {
=======
deckRouter.put('/:id', authRequired, async (req, res) => {
>>>>>>> origin/main
try {
const deckId = req.params.id;
const userId = (req as any).user.userId;
logRequest('Update deck endpoint accessed', req, res, { deckId, userId, updateFields: Object.keys(req.body) });
const result = await container.updateDeckCommandHandler.execute({ id: deckId, ...req.body });
// Convert string enum values to integers
const updateData = convertEnumValues(req.body);
const result = await container.updateDeckCommandHandler.execute({ id: deckId, ...updateData });
logRequest('Deck updated successfully', req, res, { deckId, userId });
res.json(result);
} catch (error) {
logError('Update deck endpoint error', error as Error, req, res);
// Handle enum validation errors
if (error instanceof Error && error.message.includes('Invalid deck')) {
return res.status(400).json({ error: error.message });
}
if (error instanceof Error && error.message.includes('not found')) {
return res.status(404).json({ error: 'Deck not found' });
}
@@ -172,13 +227,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 });
}
<<<<<<< HEAD
if (error instanceof Error && error.message.includes('admin')) {
return res.status(403).json({ error: 'Forbidden: ' + error.message });
}
=======
>>>>>>> origin/main
res.status(500).json({ error: 'Internal server error' });
}
@@ -206,7 +206,26 @@ gameRouter.post('/join', optionalAuth, async (req, res) => {
playerName: actualPlayerName
});
res.json(game);
// 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);
@@ -32,11 +32,7 @@ userRouter.post('/login',
logAuth('User login successful', result.user.id, { username: result.user.username }, req, res);
res.json(result);
} else {
<<<<<<< HEAD
throw new Error(`Login failed: ${result}`);
=======
return ErrorResponseService.sendUnauthorized(res, 'Invalid username or password');
>>>>>>> origin/main
}
} catch (error) {
@@ -52,12 +48,9 @@ userRouter.post('/login',
if (error.message.includes('not verified')) {
return ErrorResponseService.sendUnauthorized(res, 'Please verify your email address');
}
<<<<<<< HEAD
if (error.message.includes('restriction')) {
return ErrorResponseService.sendUnauthorized(res, 'Please verify your email address');
}
=======
>>>>>>> origin/main
if (error.message.includes('deactivated')) {
return ErrorResponseService.sendUnauthorized(res, 'Account has been deactivated');
}
@@ -94,12 +87,8 @@ userRouter.post('/create',
res.status(201).json(result);
} catch (error) {
<<<<<<< HEAD
// Don't log here since CreateUserCommandHandler already logs system errors
// Only log validation/user input errors at router level
=======
logError('Create user endpoint error', error as Error, req, res);
>>>>>>> origin/main
if (error instanceof Error) {
if (error.message.includes('already exists')) {
@@ -108,13 +97,10 @@ userRouter.post('/create',
if (error.message.includes('validation')) {
return ErrorResponseService.sendBadRequest(res, error.message);
}
<<<<<<< HEAD
// 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);
}
=======
>>>>>>> origin/main
}
return ErrorResponseService.sendInternalServerError(res);
@@ -187,7 +173,6 @@ userRouter.patch('/profile', authRequired, async (req, res) => {
}
});
<<<<<<< HEAD
//Soft delete user (current user)
userRouter.delete('/profile', authRequired, async (req, res) => {
try {
@@ -214,6 +199,32 @@ userRouter.post('/logout', authRequired, async (req, res) => {
}
});
// Refresh token endpoint
userRouter.post('/refresh-token', async (req, res) => {
try {
logRequest('Token refresh endpoint accessed', req, res);
const jwtService = container.jwtService;
const newTokenPair = jwtService.attemptTokenRefresh(req, res);
if (newTokenPair) {
logRequest('Token refresh successful', req, res);
res.json({
success: true,
message: 'Tokens refreshed successfully',
accessToken: newTokenPair.accessToken,
refreshToken: newTokenPair.refreshToken
});
} else {
logWarning('Token refresh failed - invalid or missing refresh token', undefined, req, res);
return ErrorResponseService.sendUnauthorized(res, 'Invalid or expired refresh token');
}
} catch (error) {
logError('Refresh token endpoint error', error as Error, req, res);
return ErrorResponseService.sendInternalServerError(res);
}
});
// Email verification endpoint
userRouter.get('/verify-email/:token', async (req, res) => {
try {
@@ -325,6 +336,4 @@ userRouter.post('/reset-password',
}
});
=======
>>>>>>> origin/main
export default userRouter;
@@ -1,8 +1,5 @@
import swaggerJSDoc from 'swagger-jsdoc';
<<<<<<< HEAD
import path from 'path';
=======
>>>>>>> origin/main
export const swaggerOptions = {
definition: {
@@ -22,17 +19,12 @@ export const swaggerOptions = {
},
servers: [
{
<<<<<<< HEAD
url: 'http://localhost:3001',
description: 'Local development server'
},
{
url: 'http://localhost:3000',
description: 'Local development server (alt)'
=======
url: 'http://localhost:3000',
description: 'Local development server'
>>>>>>> origin/main
},
{
url: 'https://api.serpentrace.com',
@@ -74,7 +66,6 @@ export const swaggerOptions = {
{
name: 'Deck Import/Export',
description: 'Import and export deck functionality'
<<<<<<< HEAD
},
{
name: 'Games',
@@ -99,17 +90,11 @@ export const swaggerOptions = {
{
name: 'Admin - Contacts',
description: 'Admin contact management operations'
=======
>>>>>>> origin/main
}
]
},
apis: [
<<<<<<< HEAD
'./src/Api/swagger/swaggerDefinitionsFixed.ts'
=======
'./src/Api/swagger/swaggerDefinitions.ts'
>>>>>>> origin/main
],
};
@@ -21,10 +21,6 @@ export class UserMapper {
fname: user.fname,
lname: user.lname,
code: user.token,
<<<<<<< HEAD
=======
type: user.type,
>>>>>>> origin/main
phone: user.phone,
state: user.state,
};
@@ -24,10 +24,6 @@ export interface DetailUserDto {
fname: string;
lname: string;
code: string | null;
<<<<<<< HEAD
=======
type: string;
>>>>>>> origin/main
phone: string | null;
state: number;
}
@@ -1,9 +1,6 @@
export interface UpdateDeckCommand {
id: string;
<<<<<<< HEAD
userstate?: number;
=======
>>>>>>> origin/main
name?: string;
type?: number;
userid?: string;
@@ -2,17 +2,13 @@ import { IDeckRepository } from '../../../Domain/IRepository/IDeckRepository';
import { UpdateDeckCommand } from './UpdateDeckCommand';
import { ShortDeckDto } from '../../DTOs/DeckDto';
import { DeckMapper } from '../../DTOs/Mappers/DeckMapper';
<<<<<<< HEAD
import { DeckAggregate } from '../../../Domain/Deck/DeckAggregate';
import { logError } from '../../Services/Logger';
=======
>>>>>>> origin/main
export class UpdateDeckCommandHandler {
constructor(private readonly deckRepo: IDeckRepository) {}
async execute(cmd: UpdateDeckCommand): Promise<ShortDeckDto | null> {
<<<<<<< HEAD
if(cmd.state !== undefined && cmd.userstate!==1) {
throw new Error('Only admin users can change deck state');
}
@@ -50,10 +46,5 @@ export class UpdateDeckCommandHandler {
logError(`Error updating deck: ${cmd.id}`, error);
throw error;
}
=======
const updated = await this.deckRepo.update(cmd.id, { ...cmd });
if (!updated) return null;
return DeckMapper.toShortDto(updated);
>>>>>>> origin/main
}
}
@@ -1,25 +1,14 @@
import { IDeckRepository } from '../../../Domain/IRepository/IDeckRepository';
import { GetDeckByIdQuery } from './GetDeckByIdQuery';
<<<<<<< HEAD
import { DetailDeckDto } from '../../DTOs/DeckDto';
=======
import { ShortDeckDto } from '../../DTOs/DeckDto';
>>>>>>> origin/main
import { DeckMapper } from '../../DTOs/Mappers/DeckMapper';
export class GetDeckByIdQueryHandler {
constructor(private readonly deckRepo: IDeckRepository) {}
<<<<<<< HEAD
async execute(query: GetDeckByIdQuery): Promise<DetailDeckDto | null> {
const deck = await this.deckRepo.findById(query.id);
if (!deck) return null;
return DeckMapper.toDetailDto(deck);
=======
async execute(query: GetDeckByIdQuery): Promise<ShortDeckDto | null> {
const deck = await this.deckRepo.findById(query.id);
if (!deck) return null;
return DeckMapper.toShortDto(deck);
>>>>>>> origin/main
}
}
@@ -151,6 +151,15 @@ export class JoinGameCommandHandler {
isOnline: true
};
// Check if player name is already in use by a different player
const existingPlayerWithName = gameData.currentPlayers.find(
p => p.playerName === command.playerName && p.playerId !== command.playerId
);
if (existingPlayerWithName) {
throw new Error(`Player name "${command.playerName}" is already in use in this game`);
}
// Update players list (remove if exists, then add)
gameData.currentPlayers = gameData.currentPlayers.filter(p => p.playerId !== command.playerId);
gameData.currentPlayers.push(newPlayer);
@@ -161,9 +170,6 @@ export class JoinGameCommandHandler {
// Store updated data in Redis with TTL (24 hours)
await this.redisService.setWithExpiry(redisKey, JSON.stringify(gameData), 24 * 60 * 60);
// Add player to active players set
await this.redisService.setAdd(`active_players:${game.id}`, command.playerId);
logOther('Game data updated in Redis', {
gameId: game.id,
gameCode: game.gamecode,
@@ -204,7 +210,6 @@ export class JoinGameCommandHandler {
gameData.currentPlayers = gameData.currentPlayers.filter(p => p.playerId !== playerId);
await this.redisService.setWithExpiry(redisKey, JSON.stringify(gameData), 24 * 60 * 60);
await this.redisService.setRemove(`active_players:${gameId}`, playerId);
}
} catch (error) {
logError('Failed to remove player from Redis', error instanceof Error ? error : new Error(String(error)));
@@ -64,7 +64,7 @@ export class StartGameCommandHandler {
gamecode,
maxplayers: command.maxplayers,
logintype: command.logintype,
createdby: command.userid || null,
createdby: command.userid!,
orgid: command.orgid || null,
gamedecks,
players: [],
@@ -49,7 +49,6 @@ export class GeneralSearchService implements IGeneralSearchService {
};
}
<<<<<<< HEAD
// Ensure limit is at least 1 to prevent database issues
const effectiveLimit = Math.max(limit || 20, 1);
const effectiveOffset = Math.max(offset || 0, 0);
@@ -58,12 +57,6 @@ export class GeneralSearchService implements IGeneralSearchService {
const { users, totalCount } = await this.userRepo.search(query.trim(), effectiveLimit, effectiveOffset);
const results = users.map(user => UserMapper.toShortDto(user));
const hasMore = (effectiveOffset + effectiveLimit) < totalCount;
=======
try {
const { users, totalCount } = await this.userRepo.search(query.trim(), limit, offset);
const results = users.map(user => UserMapper.toShortDto(user));
const hasMore = (offset + limit) < totalCount;
>>>>>>> origin/main
return {
results,
@@ -116,7 +109,6 @@ export class GeneralSearchService implements IGeneralSearchService {
};
}
<<<<<<< HEAD
// Ensure limit is at least 1 to prevent database issues
const effectiveLimit = Math.max(limit || 20, 1);
const effectiveOffset = Math.max(offset || 0, 0);
@@ -136,19 +128,6 @@ export class GeneralSearchService implements IGeneralSearchService {
} catch (error) {
throw new Error('Failed to search decks');
}
=======
const { decks, totalCount } = await this.deckRepo.search(query.trim(), limit, offset);
const results = decks.map(deck => DeckMapper.toShortDto(deck));
const hasMore = (offset + limit) < totalCount;
return {
results,
totalCount,
hasMore,
searchQuery: query,
searchType: 'decks'
};
>>>>>>> origin/main
}
async searchByType(
@@ -1,6 +1,5 @@
import { Request, Response, NextFunction } from 'express';
import { JWTService } from './JWTService';
<<<<<<< HEAD
import { RedisService } from './RedisService';
import { logAuth, logWarning } from './Logger';
@@ -80,7 +79,7 @@ export async function authRequired(req: Request, res: Response, next: NextFuncti
orgId: payload.orgId
}, req);
const refreshed = jwtService.refreshIfNeeded(payload, res);
const refreshed = jwtService.refreshIfNeeded(payload, res, req);
if (refreshed) {
logAuth('Token refreshed', payload.userId, undefined, req);
}
@@ -133,7 +132,7 @@ export async function adminRequired(req: Request, res: Response, next: NextFunct
orgId: payload.orgId
}, req);
const refreshed = jwtService.refreshIfNeeded(payload, res);
const refreshed = jwtService.refreshIfNeeded(payload, res, req);
if (refreshed) {
logAuth('Admin token refreshed', payload.userId, undefined, req);
}
@@ -144,60 +143,4 @@ export async function adminRequired(req: Request, res: Response, next: NextFunct
logWarning('Admin authentication middleware error', { error: (error as Error).message }, req);
return res.status(500).json({ error: 'Internal server error' });
}
=======
import { logAuth, logWarning } from './Logger';
export const jwtService = new JWTService();
export function authRequired(req: Request, res: Response, next: NextFunction) {
const payload = jwtService.verify(req);
if (!payload) {
logAuth('Authentication failed - No valid token', undefined, {
ip: req.ip,
userAgent: req.get ? req.get('User-Agent') : 'unknown',
path: req.path
}, req);
return res.status(401).json({ error: 'Unauthorized' });
}
logAuth('Authentication successful', payload.userId, {
authLevel: payload.authLevel,
orgId: payload.orgId
}, req);
const refreshed = jwtService.refreshIfNeeded(payload, res);
if (refreshed) {
logAuth('Token refreshed', payload.userId, undefined, req);
}
(req as any).user = payload;
next();
}
export function adminRequired(req: Request, res: Response, next: NextFunction) {
const payload = jwtService.verify(req);
if (!payload || payload.authLevel !== 1) {
logWarning('Admin access denied', {
hasPayload: !!payload,
authLevel: payload?.authLevel,
userId: payload?.userId,
ip: req.ip,
path: req.path
}, req);
return res.status(403).json({ error: 'Forbidden' });
}
logAuth('Admin authentication successful', payload.userId, {
authLevel: payload.authLevel,
orgId: payload.orgId
}, req);
const refreshed = jwtService.refreshIfNeeded(payload, res);
if (refreshed) {
logAuth('Admin token refreshed', payload.userId, undefined, req);
}
(req as any).user = payload;
next();
>>>>>>> origin/main
}
@@ -25,7 +25,7 @@ export class EmailTemplateHelper {
}
public static replaceTemplatePlaceholders(template: string, data: TemplateData): string {
return template.replace(/\{\{(\w+)\}\}/g, (match, key) => {
return template.replace(/\{\{\s*(\w+)\s*\}\}/g, (match, key) => {
return data[key] !== undefined ? String(data[key]) : match;
});
}
@@ -13,8 +13,11 @@ import {
GameActionData,
PlayerPosition,
GameStateUpdateData,
FieldEffectRequest
FieldEffectRequest,
JoinGameData,
LeaveGameData
} from './Interfaces/GameInterfaces';
import { json } from 'stream/consumers';
interface AuthenticatedSocket extends Socket {
userId?: string;
@@ -23,14 +26,6 @@ interface AuthenticatedSocket extends Socket {
isAuthenticated?: boolean;
}
interface JoinGameData {
gameToken: string; // Required game session token
}
interface LeaveGameData {
gameCode: string;
}
interface DiceRollData {
gameCode: string;
diceValue: number; // Value from frontend (1-6)
@@ -91,7 +86,7 @@ export class GameWebSocketService {
private setupGameEventHandlers(socket: AuthenticatedSocket): void {
// Join game room
socket.on('game:join', async (data: JoinGameData) => {
socket.on('game:join', async (data: any) => {
await this.handleJoinGame(socket, data);
});
@@ -141,11 +136,14 @@ export class GameWebSocketService {
});
}
private async handleJoinGame(socket: AuthenticatedSocket, data: JoinGameData): Promise<void> {
private async handleJoinGame(socket: AuthenticatedSocket, data: any): Promise<void> {
try {
const { gameToken } = data;
// Simple data extraction - let Socket.IO handle the parsing
const jsdata = JSON.parse(data);
const gameToken = jsdata?.gameToken;
if (!gameToken) {
logError('Game join failed: No game token provided');
socket.emit('game:error', { message: 'Game token is required' });
return;
}
@@ -153,6 +151,7 @@ export class GameWebSocketService {
// Verify the game token
const gameTokenPayload = this.gameTokenService.verifyGameToken(gameToken);
if (!gameTokenPayload) {
logError('Game join failed: Invalid game token');
socket.emit('game:error', { message: 'Invalid or expired game token' });
return;
}
@@ -162,10 +161,19 @@ export class GameWebSocketService {
// Validate game still exists
const game = await this.gameRepository.findByGameCode(gameCode);
if (!game || game.id !== gameId) {
logError(`Game join failed: Game not found - Code: ${gameCode}`);
socket.emit('game:error', { message: 'Game not found or token invalid' });
return;
}
// Check if player name is already in use by checking connected players
const connectedPlayers = await this.getConnectedPlayers(gameCode);
if (connectedPlayers.includes(playerName)) {
logOther(`Game join failed: Player name "${playerName}" already in use in game ${gameCode}`);
socket.emit('game:error', { message: `Player name "${playerName}" is already in use in this game` });
return;
}
// Set socket properties from game token
socket.gameCode = gameCode;
socket.playerName = playerName;
@@ -185,8 +193,6 @@ export class GameWebSocketService {
// Add to pending players list and notify gamemaster
await this.addToPendingPlayers(gameCode, playerName);
logOther(`Player ${playerName} requesting approval to join private game: ${gameRoomName}`);
// Send pending status to the requesting player
socket.emit('game:pending-approval', {
gameCode,
@@ -210,7 +216,6 @@ export class GameWebSocketService {
await socket.join(gameRoomName);
await socket.join(playerRoomName);
logOther(`Player ${playerName} joined game room: ${gameRoomName} (${isAuthenticated ? 'authenticated' : 'public'}) ${isGamemaster ? '[GAMEMASTER]' : ''}`);
// Send success response to the joining player
socket.emit('game:joined', {
@@ -222,6 +227,7 @@ export class GameWebSocketService {
timestamp: new Date().toISOString()
});
// Notify other players in the game (broadcast)
socket.to(gameRoomName).emit('game:player-joined', {
playerName: playerName,
@@ -230,40 +236,54 @@ export class GameWebSocketService {
timestamp: new Date().toISOString()
});
// Send current game state to the joining player
const gameState = await this.getGameState(gameCode);
socket.emit('game:state', gameState);
// Update Redis with active player connection
await this.updatePlayerConnection(gameCode, playerName, true);
} catch (error) {
logError('Error joining game', error as Error);
socket.emit('game:error', { message: 'Failed to join game' });
socket.emit('game:error', {
message: 'Failed to join game',
error: error instanceof Error ? error.message : 'Unknown error'
});
}
}
private async handleLeaveGame(socket: AuthenticatedSocket, data: LeaveGameData): Promise<void> {
try {
const { gameCode } = data;
const { gameCode } = JSON.parse(data as any);
const playerName = socket.playerName;
// Validate we have the required data
if (!playerName) {
logError('Cannot leave game: socket has no playerName');
socket.emit('game:error', { message: 'Player has no name' });
return;
}
const gameRoomName = `game_${gameCode}`;
const playerRoomName = `game_${gameCode}:${socket.playerName}`;
const playerRoomName = `game_${gameCode}:${playerName}`;
// Leave both rooms
await socket.leave(gameRoomName);
await socket.leave(playerRoomName);
logOther(`Player ${socket.playerName} left game room: ${gameRoomName}`);
logOther(`Player ${playerName} left game room: ${gameRoomName}`);
// Notify other players
socket.to(gameRoomName).emit('game:player-left', {
playerName: socket.playerName,
playerName: playerName,
timestamp: new Date().toISOString()
});
// Update Redis
await this.updatePlayerConnection(gameCode, socket.playerName!, false);
// Update Redis before clearing socket properties
await this.updatePlayerConnection(gameCode, playerName, false);
// Clear socket properties
socket.gameCode = undefined;
socket.playerName = undefined;
@@ -275,7 +295,7 @@ export class GameWebSocketService {
private async handleGameAction(socket: AuthenticatedSocket, data: GameActionData): Promise<void> {
try {
const { gameCode, action, data: actionData } = data;
const { gameCode, action, data: actionData } = JSON.parse(data as any);
if (!socket.gameCode || socket.gameCode !== gameCode) {
socket.emit('game:error', { message: 'You must be in the game to perform actions' });
@@ -317,7 +337,7 @@ export class GameWebSocketService {
private async handleGameChat(socket: AuthenticatedSocket, data: GameChatData): Promise<void> {
try {
const { gameCode, message } = data;
const { gameCode, message } = JSON.parse(data as any);
if (!socket.gameCode || socket.gameCode !== gameCode) {
socket.emit('game:error', { message: 'You must be in the game to chat' });
@@ -343,7 +363,7 @@ export class GameWebSocketService {
private async handlePlayerReady(socket: AuthenticatedSocket, data: { gameCode: string; ready: boolean }): Promise<void> {
try {
const { gameCode, ready } = data;
const { gameCode, ready } = JSON.parse(data as any);
const gameRoomName = `game_${gameCode}`;
// Update player ready status in Redis
@@ -373,7 +393,7 @@ export class GameWebSocketService {
private async handleApprovePlayer(socket: AuthenticatedSocket, data: { gameCode: string; playerName: string }): Promise<void> {
try {
const { gameCode, playerName } = data;
const { gameCode, playerName } = JSON.parse(data as any);
// Verify that the requesting socket is the gamemaster
const game = await this.gameRepository.findByGameCode(gameCode);
@@ -434,7 +454,7 @@ export class GameWebSocketService {
private async handleRejectPlayer(socket: AuthenticatedSocket, data: { gameCode: string; playerName: string; reason?: string }): Promise<void> {
try {
const { gameCode, playerName, reason } = data;
const { gameCode, playerName, reason } = JSON.parse(data as any);
// Verify that the requesting socket is the gamemaster
const game = await this.gameRepository.findByGameCode(gameCode);
@@ -482,7 +502,7 @@ export class GameWebSocketService {
private async handleJoinApproved(socket: AuthenticatedSocket, data: JoinGameData): Promise<void> {
try {
const { gameToken } = data;
const { gameToken } = JSON.parse(data as any);
if (!gameToken) {
socket.emit('game:error', { message: 'Game token is required' });
@@ -560,7 +580,7 @@ export class GameWebSocketService {
private async handleDiceRoll(socket: AuthenticatedSocket, data: DiceRollData): Promise<void> {
try {
const { gameCode, diceValue } = data;
const { gameCode, diceValue } = JSON.parse(data as any);
// Validate input
if (!gameCode || !socket.gameCode || socket.gameCode !== gameCode) {
@@ -772,14 +792,6 @@ export class GameWebSocketService {
// Remove from pending players if they were pending
await this.redisService.setRemove(`game_pending:${gameCode}`, playerName);
// If we have player ID, also clean up ID-based tracking
if (playerId) {
const game = await this.gameRepository.findByGameCode(gameCode);
if (game?.id) {
await this.redisService.setRemove(`active_players:${game.id}`, playerId);
}
}
logOther(`Cleaned up player data for ${playerName} in game ${gameCode}`);
} catch (error) {
@@ -1276,7 +1288,6 @@ export class GameWebSocketService {
if (gameId) {
const gameIdKeys = [
`game:${gameId}`, // Main game data
`active_players:${gameId}`, // Active players set
`game_turns:${gameId}` // Turn data by ID
];
@@ -7,60 +7,204 @@ export interface TokenPayload {
authLevel: 0 | 1;
userStatus: UserState;
orgId: string;
type?: 'access';
iat?: number;
exp?: number;
}
export interface RefreshTokenPayload {
userId: string;
type: 'refresh';
orgId?: string;
tokenId?: string; // For token rotation/revocation
iat?: number;
exp?: number;
}
export interface TokenPair {
accessToken: string;
refreshToken: string;
}
export class JWTService {
private readonly secretKey: string;
private readonly refreshSecretKey: string;
private readonly tokenExpiry: number;
private readonly refreshTokenExpiry: number;
private readonly cookieName: string;
private readonly refreshCookieName: string;
constructor() {
this.secretKey = process.env.JWT_SECRET || 'your-secret-key';
this.refreshSecretKey = process.env.JWT_REFRESH_SECRET || this.secretKey + '_refresh';
let expiry = 86400;
// Access token expiry (short-lived)
let expiry = 1800; // Default 30 minutes for better security
if (process.env.JWT_EXPIRY) {
expiry = parseInt(process.env.JWT_EXPIRY);
} else if (process.env.JWT_EXPIRATION) {
expiry = this.parseDuration(process.env.JWT_EXPIRATION);
}
// Refresh token expiry (long-lived)
let refreshExpiry = 604800; // Default 7 days
if (process.env.JWT_REFRESH_EXPIRATION) {
refreshExpiry = this.parseDuration(process.env.JWT_REFRESH_EXPIRATION);
}
this.tokenExpiry = expiry;
this.refreshTokenExpiry = refreshExpiry;
this.cookieName = 'auth_token';
this.refreshCookieName = 'refresh_token';
if (process.env.NODE_ENV === 'production' && (!process.env.JWT_SECRET || process.env.JWT_SECRET === 'your-secret-key')) {
throw new Error('JWT_SECRET environment variable must be set in production');
}
}
create(payload: TokenPayload, res: Response): string {
/**
* Create a pair of access and refresh tokens
*/
public createTokenPair(payload: Omit<TokenPayload, 'type' | 'iat' | 'exp'>): TokenPair {
const now = Math.floor(Date.now() / 1000);
const payloadWithTimestamps: TokenPayload = {
// Create access token
const accessTokenPayload: TokenPayload = {
...payload,
type: 'access',
iat: now,
exp: now + this.tokenExpiry
};
const accessToken = jwt.sign(accessTokenPayload, this.secretKey);
// Don't use expiresIn option since we're manually setting exp in payload
const options: SignOptions = {};
const token = jwt.sign(payloadWithTimestamps, this.secretKey, options);
// Create refresh token
const refreshTokenPayload: RefreshTokenPayload = {
userId: payload.userId,
type: 'refresh',
orgId: payload.orgId,
iat: now,
exp: now + this.refreshTokenExpiry
};
const refreshToken = jwt.sign(refreshTokenPayload, this.refreshSecretKey);
res.cookie(this.cookieName, token, {
return { accessToken, refreshToken };
}
/**
* Create access and refresh tokens and set cookies (for cookie-based auth)
*/
create(payload: Omit<TokenPayload, 'type' | 'iat' | 'exp'>, res: Response): TokenPair {
const tokenPair = this.createTokenPair(payload);
this.setTokenCookies(res, tokenPair);
return tokenPair;
}
/**
* Check if the request is using Bearer token authentication
*/
private isUsingBearerAuth(req: Request): boolean {
// No cookie but has Authorization header
return !req.cookies?.[this.cookieName] &&
!!req.headers.authorization &&
req.headers.authorization.startsWith('Bearer ');
}
/**
* Verify a refresh token
*/
public verifyRefreshToken(token: string): RefreshTokenPayload | null {
try {
const decoded = jwt.verify(token, this.refreshSecretKey) as RefreshTokenPayload;
if (decoded.type !== 'refresh') {
return null;
}
return decoded;
} catch (error) {
return null;
}
}
/**
* Attempt to refresh tokens using refresh token from cookies or headers
*/
public attemptTokenRefresh(req: Request, res: Response): TokenPair | null {
try {
// Try to get refresh token from cookie first
let refreshToken = req.cookies[this.refreshCookieName];
// If no cookie, try X-Refresh-Token header
if (!refreshToken) {
refreshToken = req.headers['x-refresh-token'] as string;
}
if (!refreshToken) {
return null;
}
const refreshPayload = this.verifyRefreshToken(refreshToken);
if (!refreshPayload) {
return null;
}
// Create new token pair
const newTokenPair = this.createTokenPair({
userId: refreshPayload.userId,
authLevel: 0, // Default auth level, should be fetched from user data
userStatus: UserState.VERIFIED_REGULAR, // Default status, should be fetched from user data
orgId: refreshPayload.orgId || ''
});
// Set new tokens based on authentication method
if (req.cookies[this.cookieName] || req.cookies[this.refreshCookieName]) {
// Cookie-based auth: set new cookies
this.setTokenCookies(res, newTokenPair);
} else {
// Header-based auth: send tokens in response headers
res.setHeader('X-New-Access-Token', newTokenPair.accessToken);
res.setHeader('X-New-Refresh-Token', newTokenPair.refreshToken);
res.setHeader('X-Token-Refreshed', 'true');
}
return newTokenPair;
} catch (error) {
return null;
}
}
/**
* Set token cookies for cookie-based authentication
*/
private setTokenCookies(res: Response, tokenPair: TokenPair): void {
// Set access token cookie
res.cookie(this.cookieName, tokenPair.accessToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: this.tokenExpiry * 1000, // Convert to milliseconds
maxAge: this.tokenExpiry * 1000,
});
return token;
// Set refresh token cookie
res.cookie(this.refreshCookieName, tokenPair.refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: this.refreshTokenExpiry * 1000,
});
}
verify(req: Request): TokenPayload | null {
try {
const token = req.cookies[this.cookieName];
// First try to get token from cookie
let token = req.cookies[this.cookieName];
// If no cookie token, try Authorization header
if (!token) {
const authHeader = req.headers.authorization;
if (authHeader && authHeader.startsWith('Bearer ')) {
token = authHeader.substring(7);
}
}
if (!token) return null;
const decoded = jwt.verify(token, this.secretKey) as TokenPayload;
@@ -70,6 +214,32 @@ export class JWTService {
}
}
/**
* Logout user by clearing tokens
*/
public logout(req: Request, res: Response): void {
// Clear cookies if they exist
if (req.cookies[this.cookieName]) {
res.clearCookie(this.cookieName, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict'
});
}
if (req.cookies[this.refreshCookieName]) {
res.clearCookie(this.refreshCookieName, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict'
});
}
// For bearer token auth, set headers to indicate logout
res.setHeader('X-Auth-Logout', 'true');
res.setHeader('X-Clear-Tokens', 'true');
}
// Check if token needs refresh (within 25% of expiry time)
shouldRefreshToken(payload: TokenPayload): boolean {
if (!payload.exp || !payload.iat) return false;
@@ -83,16 +253,39 @@ export class JWTService {
}
// Conditionally refresh token only if needed
refreshIfNeeded(payload: TokenPayload, res: Response): boolean {
refreshIfNeeded(payload: TokenPayload, res: Response, req?: Request): boolean {
if (this.shouldRefreshToken(payload)) {
// Create new token with fresh timestamps, but same user data
const freshPayload: Omit<TokenPayload, 'iat' | 'exp'> = {
if (req) {
// Try to use the new refresh token system
const newTokenPair = this.attemptTokenRefresh(req, res);
if (newTokenPair) {
return true;
}
}
// Fallback: create new token pair
const freshPayload: Omit<TokenPayload, 'iat' | 'exp' | 'type'> = {
userId: payload.userId,
authLevel: payload.authLevel,
userStatus: payload.userStatus,
orgId: payload.orgId
};
this.create(freshPayload, res);
// Check if using Bearer authentication
if (req && this.isUsingBearerAuth(req)) {
// For Bearer auth, create token pair and add to headers
const newTokenPair = this.createTokenPair(freshPayload);
res.setHeader('X-New-Access-Token', newTokenPair.accessToken);
res.setHeader('X-New-Refresh-Token', newTokenPair.refreshToken);
res.setHeader('X-Token-Refreshed', 'true');
} else {
// For cookie auth, create token pair and set cookies
const newTokenPair = this.create(freshPayload, res);
res.setHeader('X-New-Access-Token', newTokenPair.accessToken);
res.setHeader('X-New-Refresh-Token', newTokenPair.refreshToken);
res.setHeader('X-Token-Refreshed', 'true');
}
return true;
}
return false;
@@ -12,6 +12,7 @@ import { Response } from 'express';
export interface LoginResponse {
user: ShortUserDto;
token: string;
refreshToken: string;
requiresOrgReauth?: boolean;
orgLoginUrl?: string;
organizationName?: string;
@@ -111,7 +112,23 @@ export class LoginCommandHandler {
try {
// Use the real response object if provided, otherwise use mock
const responseObj = res || mockRes;
const token = this.jwtService.create(tokenPayload, responseObj);
// Check if client prefers Bearer token authentication
const prefersBearerAuth = res && (
res.req?.headers['authorization'] !== undefined ||
res.req?.headers['x-auth-method'] === 'bearer' ||
res.req?.headers['accept']?.includes('application/json')
);
let tokenPair: any;
if (prefersBearerAuth && res) {
// Create token pair for Bearer authentication (no cookies)
tokenPair = this.jwtService.createTokenPair(tokenPayload);
} else {
// Cookie-based authentication (sets cookies automatically)
tokenPair = this.jwtService.create(tokenPayload, responseObj);
}
// Check if user belongs to an organization and needs reauthentication
let requiresOrgReauth = false;
@@ -154,7 +171,8 @@ export class LoginCommandHandler {
const response: LoginResponse = {
user: UserMapper.toShortDto(user),
token
token: tokenPair.accessToken,
refreshToken: tokenPair.refreshToken
};
if (requiresOrgReauth) {
@@ -17,45 +17,63 @@ export class LogoutCommandHandler {
try {
logAuth('Logout process started', userId);
// 1. Get token from request to blacklist it
let tokenToBlacklist: string | null = null;
// 1. Get tokens from request to blacklist them
let accessTokenToBlacklist: string | null = null;
let refreshTokenToBlacklist: string | null = null;
if (req) {
// Extract token from cookie
tokenToBlacklist = req.cookies['auth_token'];
// Also check Authorization header as fallback
if (!tokenToBlacklist && req.headers.authorization) {
// Extract access token from cookie or Authorization header
accessTokenToBlacklist = req.cookies['auth_token'];
if (!accessTokenToBlacklist && req.headers.authorization) {
const authHeader = req.headers.authorization;
if (authHeader.startsWith('Bearer ')) {
tokenToBlacklist = authHeader.substring(7);
accessTokenToBlacklist = authHeader.substring(7);
}
}
// Extract refresh token from cookie or header
refreshTokenToBlacklist = req.cookies['refresh_token'];
if (!refreshTokenToBlacklist) {
refreshTokenToBlacklist = req.headers['x-refresh-token'] as string;
}
}
// 2. Blacklist the current JWT token in Redis (if available)
if (tokenToBlacklist && req) {
// 2. Blacklist both access and refresh tokens in Redis
if (accessTokenToBlacklist && req) {
try {
// Store token in blacklist with expiration matching token expiry
const decoded = this.jwtService.verify(req);
if (decoded && decoded.exp) {
const ttl = decoded.exp - Math.floor(Date.now() / 1000);
if (ttl > 0) {
await this.redisService.setWithExpiry(`blacklist:${tokenToBlacklist}`, 'true', ttl);
logAuth('JWT token blacklisted', userId, { tokenExpiry: ttl });
await this.redisService.setWithExpiry(`blacklist:${accessTokenToBlacklist}`, 'true', ttl);
logAuth('Access token blacklisted', userId, { tokenExpiry: ttl });
}
}
} catch (error) {
logWarning('Failed to blacklist token', { userId, error: (error as Error).message });
logWarning('Failed to blacklist access token', { userId, error: (error as Error).message });
}
}
// 3. Clear authentication cookie
res.clearCookie('auth_token', {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
path: '/'
});
// Blacklist refresh token if present
if (refreshTokenToBlacklist) {
try {
const refreshDecoded = this.jwtService.verifyRefreshToken(refreshTokenToBlacklist);
if (refreshDecoded && refreshDecoded.exp) {
const ttl = refreshDecoded.exp - Math.floor(Date.now() / 1000);
if (ttl > 0) {
await this.redisService.setWithExpiry(`blacklist:${refreshTokenToBlacklist}`, 'true', ttl);
logAuth('Refresh token blacklisted', userId, { tokenExpiry: ttl });
}
}
} catch (error) {
logWarning('Failed to blacklist refresh token', { userId, error: (error as Error).message });
}
}
// 3. Use JWT service to clear cookies and set logout headers
if (req) {
this.jwtService.logout(req, res);
}
// 4. Remove user from active sessions in Redis
try {
@@ -50,16 +50,19 @@ export class GameAggregate {
@Column({ type: 'int', default: LoginType.PUBLIC })
logintype!: LoginType;
@Column({ type: 'varchar', length: 255, nullable: true })
createdby!: string | null;
@Column({ type: 'int', default: 50 })
boardsize!: number;
@Column({ type: 'varchar', length: 255, nullable: true })
@Column({ type: 'uuid', nullable: false, name: 'createdBy' })
createdby!: string;
@Column({ type: 'uuid', nullable: true, name: 'organizationid' })
orgid!: string | null;
@Column({ type: 'json' })
@Column({ type: 'jsonb', default: () => "'[]'", name: 'decks' })
gamedecks!: GameDeck[];
@Column({ type: 'json', default: () => "'[]'" })
@Column({ type: 'uuid', array: true, default: () => "'{}'", name: 'playerids' })
players!: string[];
@Column({ type: 'boolean', default: false })
@@ -68,22 +71,22 @@ export class GameAggregate {
@Column({ type: 'boolean', default: false })
finished!: boolean;
@Column({ type: 'varchar', length: 255, nullable: true })
@Column({ type: 'uuid', nullable: true, name: 'winnerid' })
winner!: string | null;
@Column({ type: 'int', default: GameState.WAITING })
state!: GameState;
@CreateDateColumn({ name: 'create_date' })
@CreateDateColumn({ name: 'createDate' })
createdate!: Date;
@Column({ type: 'timestamp', nullable: true, name: 'start_date' })
startdate!: Date | null;
@Column({ type: 'timestamp', nullable: true, name: 'end_date' })
@Column({ type: 'timestamp', nullable: true, name: 'finishDate' })
enddate!: Date | null;
@UpdateDateColumn({ name: 'update_date' })
@UpdateDateColumn({ name: 'updateDate' })
updatedate!: Date;
}
@@ -1,22 +0,0 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class Test1755691733404 implements MigrationInterface {
name = 'Test1755691733404'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "Users" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "orgid" uuid, "username" character varying(100) NOT NULL, "password" character varying(255) NOT NULL, "email" character varying(255) NOT NULL, "fname" character varying(100) NOT NULL, "lname" character varying(100) NOT NULL, "code" character varying(50), "type" character varying(50) NOT NULL, "phone" character varying(20), "state" integer NOT NULL DEFAULT '0', "regdate" TIMESTAMP NOT NULL DEFAULT now(), "updatedate" TIMESTAMP NOT NULL DEFAULT now(), "Orglogindate" TIMESTAMP, CONSTRAINT "UQ_ffc81a3b97dcbf8e320d5106c0d" UNIQUE ("username"), CONSTRAINT "UQ_3c3ab3f49a87e6ddb607f3c4945" UNIQUE ("email"), CONSTRAINT "PK_16d4f7d636df336db11d87413e3" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE TABLE "Organizations" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying(255) NOT NULL, "contactfname" character varying(100) NOT NULL, "contactlname" character varying(100) NOT NULL, "contactphone" character varying(20) NOT NULL, "contactemail" character varying(255) NOT NULL, "state" integer NOT NULL DEFAULT '0', "regdate" TIMESTAMP NOT NULL DEFAULT now(), "updatedate" TIMESTAMP NOT NULL DEFAULT now(), "url" character varying(500), "userinorg" integer NOT NULL DEFAULT '0', CONSTRAINT "PK_e0690a31419f6666194423526f2" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE TABLE "Decks" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying(255) NOT NULL, "type" integer NOT NULL, "user_id" uuid NOT NULL, "creation_date" TIMESTAMP NOT NULL DEFAULT now(), "cards" json NOT NULL, "played_number" integer NOT NULL DEFAULT '0', "ctype" integer NOT NULL DEFAULT '0', "update_date" TIMESTAMP NOT NULL DEFAULT now(), "state" integer NOT NULL DEFAULT '0', "organization_id" uuid, CONSTRAINT "PK_001f26cb3ec39c1f25269943473" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE TABLE "Chats" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "users" uuid array NOT NULL, "messages" json NOT NULL, "updateDate" TIMESTAMP NOT NULL DEFAULT now(), "state" integer NOT NULL DEFAULT '0', CONSTRAINT "PK_64c36c2b8d86a0d5de4cf64de8d" PRIMARY KEY ("id"))`);
await queryRunner.query(`ALTER TABLE "Decks" ADD CONSTRAINT "FK_06ee28f90d68543a03b14aebe13" FOREIGN KEY ("organization_id") REFERENCES "Organizations"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "Decks" DROP CONSTRAINT "FK_06ee28f90d68543a03b14aebe13"`);
await queryRunner.query(`DROP TABLE "Chats"`);
await queryRunner.query(`DROP TABLE "Decks"`);
await queryRunner.query(`DROP TABLE "Organizations"`);
await queryRunner.query(`DROP TABLE "Users"`);
}
}
@@ -1,18 +0,0 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddEmailVerificationFields1755706019351 implements MigrationInterface {
name = 'AddEmailVerificationFields1755706019351'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "Users" DROP COLUMN "code"`);
await queryRunner.query(`ALTER TABLE "Users" ADD "token" character varying(255)`);
await queryRunner.query(`ALTER TABLE "Users" ADD "TokenExpires" TIMESTAMP`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "Users" DROP COLUMN "TokenExpires"`);
await queryRunner.query(`ALTER TABLE "Users" DROP COLUMN "token"`);
await queryRunner.query(`ALTER TABLE "Users" ADD "code" character varying(50)`);
}
}
@@ -1,30 +0,0 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddChatMessagingSystem1755817306222 implements MigrationInterface {
name = 'AddChatMessagingSystem1755817306222'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "ChatArchives" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "chatId" uuid NOT NULL, "archivedMessages" json NOT NULL, "archivedAt" TIMESTAMP NOT NULL, "createDate" TIMESTAMP NOT NULL DEFAULT now(), "chatType" character varying(50) NOT NULL, "chatName" character varying(255), "gameId" uuid, "participants" uuid array NOT NULL, CONSTRAINT "PK_fe62979fc2061d7afe278d3f14e" PRIMARY KEY ("id"))`);
await queryRunner.query(`ALTER TABLE "Chats" ADD "type" character varying(50) NOT NULL DEFAULT 'direct'`);
await queryRunner.query(`ALTER TABLE "Chats" ADD "name" character varying(255)`);
await queryRunner.query(`ALTER TABLE "Chats" ADD "gameId" uuid`);
await queryRunner.query(`ALTER TABLE "Chats" ADD "createdBy" uuid`);
await queryRunner.query(`ALTER TABLE "Chats" ADD "lastActivity" TIMESTAMP`);
await queryRunner.query(`ALTER TABLE "Chats" ADD "createDate" TIMESTAMP NOT NULL DEFAULT now()`);
await queryRunner.query(`ALTER TABLE "Chats" ADD "archiveDate" TIMESTAMP`);
await queryRunner.query(`ALTER TABLE "Chats" ALTER COLUMN "messages" SET DEFAULT '[]'`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "Chats" ALTER COLUMN "messages" DROP DEFAULT`);
await queryRunner.query(`ALTER TABLE "Chats" DROP COLUMN "archiveDate"`);
await queryRunner.query(`ALTER TABLE "Chats" DROP COLUMN "createDate"`);
await queryRunner.query(`ALTER TABLE "Chats" DROP COLUMN "lastActivity"`);
await queryRunner.query(`ALTER TABLE "Chats" DROP COLUMN "createdBy"`);
await queryRunner.query(`ALTER TABLE "Chats" DROP COLUMN "gameId"`);
await queryRunner.query(`ALTER TABLE "Chats" DROP COLUMN "name"`);
await queryRunner.query(`ALTER TABLE "Chats" DROP COLUMN "type"`);
await queryRunner.query(`DROP TABLE "ChatArchives"`);
}
}
@@ -1,14 +0,0 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class CreateContactTable1755855028839 implements MigrationInterface {
name = 'CreateContactTable1755855028839'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "Contacts" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying(255) NOT NULL, "email" character varying(255) NOT NULL, "userid" uuid, "type" integer NOT NULL, "txt" text NOT NULL, "state" integer NOT NULL DEFAULT '0', "createDate" TIMESTAMP NOT NULL DEFAULT now(), "updateDate" TIMESTAMP NOT NULL DEFAULT now(), "adminResponse" text, "responseDate" TIMESTAMP, "respondedBy" uuid, CONSTRAINT "PK_68782cec65c8eef577c62958273" PRIMARY KEY ("id"))`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE "Contacts"`);
}
}
@@ -1,28 +0,0 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class Full1757939815984 implements MigrationInterface {
name = 'Full1757939815984'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "Chats" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "type" character varying(50) NOT NULL DEFAULT 'direct', "name" character varying(255), "gameId" uuid, "createdBy" uuid, "users" uuid array NOT NULL, "messages" json NOT NULL DEFAULT '[]', "lastActivity" TIMESTAMP, "createDate" TIMESTAMP NOT NULL DEFAULT now(), "updateDate" TIMESTAMP NOT NULL DEFAULT now(), "state" integer NOT NULL DEFAULT '0', "archiveDate" TIMESTAMP, CONSTRAINT "PK_64c36c2b8d86a0d5de4cf64de8d" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE TABLE "Users" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "orgid" uuid, "username" character varying(100) NOT NULL, "password" character varying(255) NOT NULL, "email" character varying(255) NOT NULL, "fname" character varying(100) NOT NULL, "lname" character varying(100) NOT NULL, "token" character varying(255), "TokenExpires" TIMESTAMP, "phone" character varying(20), "state" integer NOT NULL DEFAULT '0', "regdate" TIMESTAMP NOT NULL DEFAULT now(), "updatedate" TIMESTAMP NOT NULL DEFAULT now(), "Orglogindate" TIMESTAMP, CONSTRAINT "UQ_ffc81a3b97dcbf8e320d5106c0d" UNIQUE ("username"), CONSTRAINT "UQ_3c3ab3f49a87e6ddb607f3c4945" UNIQUE ("email"), CONSTRAINT "PK_16d4f7d636df336db11d87413e3" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE TABLE "Contacts" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying(255) NOT NULL, "email" character varying(255) NOT NULL, "userid" uuid, "type" integer NOT NULL, "txt" text NOT NULL, "state" integer NOT NULL DEFAULT '0', "createDate" TIMESTAMP NOT NULL DEFAULT now(), "updateDate" TIMESTAMP NOT NULL DEFAULT now(), "adminResponse" text, "responseDate" TIMESTAMP, "respondedBy" uuid, CONSTRAINT "PK_68782cec65c8eef577c62958273" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE TABLE "ChatArchives" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "chatId" uuid NOT NULL, "archivedMessages" json NOT NULL, "archivedAt" TIMESTAMP NOT NULL, "createDate" TIMESTAMP NOT NULL DEFAULT now(), "chatType" character varying(50) NOT NULL, "chatName" character varying(255), "gameId" uuid, "participants" uuid array NOT NULL, CONSTRAINT "PK_fe62979fc2061d7afe278d3f14e" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE TABLE "Games" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "gamecode" character varying(10) NOT NULL, "maxplayers" integer NOT NULL, "logintype" integer NOT NULL DEFAULT '0', "createdby" character varying(255), "orgid" character varying(255), "gamedecks" json NOT NULL, "players" json NOT NULL DEFAULT '[]', "started" boolean NOT NULL DEFAULT false, "finished" boolean NOT NULL DEFAULT false, "winner" character varying(255), "state" integer NOT NULL DEFAULT '0', "create_date" TIMESTAMP NOT NULL DEFAULT now(), "start_date" TIMESTAMP, "end_date" TIMESTAMP, "update_date" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "UQ_9d52c646079cbe6f242a85c5c41" UNIQUE ("gamecode"), CONSTRAINT "PK_1950492f583d31609c5e9fbbe12" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE TABLE "Organizations" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying(255) NOT NULL, "contactfname" character varying(100) NOT NULL, "contactlname" character varying(100) NOT NULL, "contactphone" character varying(20) NOT NULL, "contactemail" character varying(255) NOT NULL, "state" integer NOT NULL DEFAULT '0', "regdate" TIMESTAMP NOT NULL DEFAULT now(), "updatedate" TIMESTAMP NOT NULL DEFAULT now(), "url" character varying(500), "userinorg" integer NOT NULL DEFAULT '0', "maxOrganizationalDecks" integer, CONSTRAINT "PK_e0690a31419f6666194423526f2" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE TABLE "Decks" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying(255) NOT NULL, "type" integer NOT NULL, "user_id" uuid NOT NULL, "creation_date" TIMESTAMP NOT NULL DEFAULT now(), "cards" json NOT NULL, "played_number" integer NOT NULL DEFAULT '0', "ctype" integer NOT NULL DEFAULT '0', "update_date" TIMESTAMP NOT NULL DEFAULT now(), "state" integer NOT NULL DEFAULT '0', "organization_id" uuid, CONSTRAINT "PK_001f26cb3ec39c1f25269943473" PRIMARY KEY ("id"))`);
await queryRunner.query(`ALTER TABLE "Decks" ADD CONSTRAINT "FK_06ee28f90d68543a03b14aebe13" FOREIGN KEY ("organization_id") REFERENCES "Organizations"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "Decks" DROP CONSTRAINT "FK_06ee28f90d68543a03b14aebe13"`);
await queryRunner.query(`DROP TABLE "Decks"`);
await queryRunner.query(`DROP TABLE "Organizations"`);
await queryRunner.query(`DROP TABLE "Games"`);
await queryRunner.query(`DROP TABLE "ChatArchives"`);
await queryRunner.query(`DROP TABLE "Contacts"`);
await queryRunner.query(`DROP TABLE "Users"`);
await queryRunner.query(`DROP TABLE "Chats"`);
}
}
@@ -0,0 +1,30 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class Full1758463929834 implements MigrationInterface {
name = 'Full1758463929834'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "Games" DROP COLUMN "winner"`);
await queryRunner.query(`ALTER TABLE "Games" DROP COLUMN "create_date"`);
await queryRunner.query(`ALTER TABLE "Games" DROP COLUMN "end_date"`);
await queryRunner.query(`ALTER TABLE "Games" DROP COLUMN "update_date"`);
await queryRunner.query(`ALTER TABLE "Games" ADD "boardsize" integer NOT NULL DEFAULT '50'`);
await queryRunner.query(`ALTER TABLE "Games" ADD "winnerid" uuid`);
await queryRunner.query(`ALTER TABLE "Games" ADD "createDate" TIMESTAMP NOT NULL DEFAULT now()`);
await queryRunner.query(`ALTER TABLE "Games" ADD "finishDate" TIMESTAMP`);
await queryRunner.query(`ALTER TABLE "Games" ADD "updateDate" TIMESTAMP NOT NULL DEFAULT now()`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "Games" DROP COLUMN "updateDate"`);
await queryRunner.query(`ALTER TABLE "Games" DROP COLUMN "finishDate"`);
await queryRunner.query(`ALTER TABLE "Games" DROP COLUMN "createDate"`);
await queryRunner.query(`ALTER TABLE "Games" DROP COLUMN "winnerid"`);
await queryRunner.query(`ALTER TABLE "Games" DROP COLUMN "boardsize"`);
await queryRunner.query(`ALTER TABLE "Games" ADD "update_date" TIMESTAMP NOT NULL DEFAULT now()`);
await queryRunner.query(`ALTER TABLE "Games" ADD "end_date" TIMESTAMP`);
await queryRunner.query(`ALTER TABLE "Games" ADD "create_date" TIMESTAMP NOT NULL DEFAULT now()`);
await queryRunner.query(`ALTER TABLE "Games" ADD "winner" character varying(255)`);
}
}
@@ -1,28 +0,0 @@
import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';
export class AddMaxOrganizationalDecksToOrganization1692712800000 implements MigrationInterface {
name = 'AddMaxOrganizationalDecksToOrganization1692712800000';
public async up(queryRunner: QueryRunner): Promise<void> {
// Add maxOrganizationalDecks column to Organizations table
await queryRunner.addColumn('Organizations', new TableColumn({
name: 'maxOrganizationalDecks',
type: 'int',
isNullable: true, // No default - set by admin
comment: 'Maximum number of organizational decks a premium user can create in this organization'
}));
// Add performance indexes for deck filtering queries
await queryRunner.query(`CREATE INDEX "IDX_DECK_USER_STATE_CTYPE" ON "Decks" ("user_id", "state", "ctype")`);
await queryRunner.query(`CREATE INDEX "IDX_DECK_ORG_CTYPE_STATE" ON "Decks" ("organization_id", "ctype", "state")`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
// Drop indexes
await queryRunner.query(`DROP INDEX "IDX_DECK_ORG_CTYPE_STATE"`);
await queryRunner.query(`DROP INDEX "IDX_DECK_USER_STATE_CTYPE"`);
// Remove maxOrganizationalDecks column
await queryRunner.dropColumn('Organizations', 'maxOrganizationalDecks');
}
}
@@ -1,11 +0,0 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddEmailVerificationFields1755706017175 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
}
public async down(queryRunner: QueryRunner): Promise<void> {
}
}
@@ -1,11 +0,0 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class FixEmailVerificationFields1755706055220 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
}
public async down(queryRunner: QueryRunner): Promise<void> {
}
}
@@ -1,11 +0,0 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class Full1757939815062 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
}
public async down(queryRunner: QueryRunner): Promise<void> {
}
}
@@ -1,6 +1,6 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class Test1755691732089 implements MigrationInterface {
export class Full1758463928499 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
}