final changes
This commit is contained in:
@@ -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"`);
|
||||
}
|
||||
|
||||
}
|
||||
-18
@@ -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)`);
|
||||
}
|
||||
|
||||
}
|
||||
-30
@@ -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)`);
|
||||
}
|
||||
|
||||
}
|
||||
-28
@@ -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');
|
||||
}
|
||||
}
|
||||
-11
@@ -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> {
|
||||
}
|
||||
|
||||
}
|
||||
-11
@@ -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
-1
@@ -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> {
|
||||
}
|
||||
Reference in New Issue
Block a user