Merge remote-tracking branch 'origin/main'

This commit is contained in:
2025-09-22 11:26:43 +02:00
789 changed files with 14011 additions and 16477 deletions
@@ -195,6 +195,7 @@ deckRouter.get('/:id', authRequired, async (req, res) => {
}
});
deckRouter.patch('/:id', authRequired, async (req, res) => {
deckRouter.patch('/:id', authRequired, async (req, res) => {
try {
const deckId = req.params.id;
@@ -228,6 +229,10 @@ deckRouter.patch('/:id', authRequired, async (req, res) => {
return res.status(400).json({ error: 'Invalid input data', details: error.message });
}
if (error instanceof Error && error.message.includes('admin')) {
return res.status(403).json({ error: 'Forbidden: ' + error.message });
}
if (error instanceof Error && error.message.includes('admin')) {
return res.status(403).json({ error: 'Forbidden: ' + error.message });
}
@@ -198,6 +198,7 @@ userRouter.post('/logout', authRequired, async (req, res) => {
return ErrorResponseService.sendInternalServerError(res);
}
});
<<<<<<< HEAD
// Refresh token endpoint
userRouter.post('/refresh-token', async (req, res) => {
@@ -336,4 +337,6 @@ userRouter.post('/reset-password',
}
});
=======
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
export default userRouter;
@@ -2,6 +2,7 @@
* @swagger
* components:
<<<<<<< HEAD
<<<<<<< HEAD
=======
* securitySchemes:
* bearerAuth:
@@ -9,6 +10,8 @@
* scheme: bearer
* bearerFormat: JWT
>>>>>>> origin/main
=======
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
* schemas:
* User:
* type: object
@@ -337,6 +340,9 @@
* type: string
*
<<<<<<< HEAD
<<<<<<< HEAD
=======
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
* Game:
* type: object
* properties:
@@ -365,8 +371,11 @@
* type: string
* format: date-time
*
<<<<<<< HEAD
=======
>>>>>>> origin/main
=======
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
* Error:
* type: object
* properties:
@@ -378,6 +387,9 @@
* details:
* type: string
<<<<<<< HEAD
<<<<<<< HEAD
=======
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
*/
/**
* @swagger
@@ -396,6 +408,7 @@
* responses:
* 200:
* description: Login successful
<<<<<<< HEAD
* content:
* application/json:
* schema:
@@ -435,6 +448,19 @@
* schema:
* $ref: '#/components/schemas/Error'
>>>>>>> origin/main
=======
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/LoginResponse'
* 401:
* description: Invalid credentials
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
*
* /api/users/create:
* post:
@@ -1498,6 +1524,9 @@
* schema:
* $ref: '#/components/schemas/Contact'
<<<<<<< HEAD
<<<<<<< HEAD
=======
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
*
* /api/games/start:
* post:
@@ -1655,8 +1684,11 @@
* description: Game already started or not ready to start
* 500:
* description: Internal server error
<<<<<<< HEAD
=======
>>>>>>> origin/main
=======
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
*/
export {};
@@ -100,6 +100,7 @@
* type: string
* format: email
*
<<<<<<< HEAD
* ForgotPasswordRequest:
* type: object
* required:
@@ -131,6 +132,8 @@
* message:
* type: string
*
=======
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
* Organization:
* type: object
* properties:
@@ -460,6 +463,7 @@
/**
* @swagger
<<<<<<< HEAD
* /api/users/verify-email/{token}:
* get:
* tags: [Users]
@@ -539,6 +543,8 @@
/**
* @swagger
=======
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
* /api/organizations/search:
* get:
* tags: [Organizations]
@@ -1,3 +1,5 @@
import { n } from "framer-motion/dist/types.d-D0HXPxHm";
export interface UpdateDeckCommand {
id: string;
userstate?: number;
@@ -1,17 +1,32 @@
import { GameField, BoardData } from '../../Domain/Game/GameAggregate';
import { logOther, logError } from '../Services/Logger';
<<<<<<< HEAD
=======
interface TargetField {
fieldNumber: number;
distance: number;
}
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
interface SpecialFieldInfo {
position: number;
type: 'positive' | 'negative' | 'luck';
}
export class BoardGenerationService {
<<<<<<< HEAD
=======
private readonly MAX_GENERATION_TIME = parseInt(process.env.MAX_GENERATION_TIME_SECONDS || '20') * 1000;
private readonly ERROR_TOLERANCE = parseInt(process.env.GENERATION_ERROR_TOLERANCE || '15');
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
async generateBoard(
positiveFieldCount: number,
negativeFieldCount: number,
luckFieldCount: number
): Promise<BoardData> {
<<<<<<< HEAD
// Pattern-based approach has 100% success rate, no retry needed
const result = this.generateSingleAttempt(positiveFieldCount, negativeFieldCount, luckFieldCount);
@@ -24,6 +39,36 @@ export class BoardGenerationService {
});
return result;
=======
const startTime = Date.now();
let bestAttempt: BoardData | null = null;
let attemptCount = 0;
while (Date.now() - startTime < this.MAX_GENERATION_TIME) {
attemptCount++;
try {
const attempt = this.generateSingleAttempt(positiveFieldCount, negativeFieldCount, luckFieldCount);
if (attempt.totalErrorRate <= this.ERROR_TOLERANCE) {
logOther(`Board generation successful on attempt ${attemptCount}. Error rate: ${attempt.totalErrorRate}%`);
return attempt;
}
if (!bestAttempt || attempt.totalErrorRate < bestAttempt.totalErrorRate) {
bestAttempt = attempt;
}
logOther(`Attempt ${attemptCount}: Error rate ${attempt.totalErrorRate}% (target: ${this.ERROR_TOLERANCE}%)`);
} catch (error) {
logError(`Board generation attempt ${attemptCount} failed:`, error as Error);
}
}
logOther(`Using best attempt with error rate: ${bestAttempt?.totalErrorRate || 100}%`);
return bestAttempt || this.generateFallbackBoard(positiveFieldCount, negativeFieldCount, luckFieldCount);
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
}
private generateSingleAttempt(
@@ -38,11 +83,42 @@ export class BoardGenerationService {
luckFieldCount
);
<<<<<<< HEAD
// Step 2: Calculate step values using pattern-based approach
const fields = this.calculatePatternBasedStepValues(specialFieldPositions);
return {
fields
=======
// Step 2: Select target fields for each special field (6 targets per field for dice 1-6)
const targetFieldsMap = this.selectTargetFields(specialFieldPositions);
// Step 3: Create border with strategic placement
const border = this.createStrategicBorder(targetFieldsMap);
// Step 4: Calculate step values based on border positions
const fields = this.calculateStepValues(specialFieldPositions, targetFieldsMap, border);
// Step 5: Validate against 20-30 rule and calculate error rate
const validationResults = this.validateBoardGeneration(fields, border);
// Log generation statistics
logOther('Board generation attempt completed', {
totalFields: fields.length,
specialFields: fields.filter(f => f.type !== 'regular').length,
positiveFields: fields.filter(f => f.type === 'positive').length,
negativeFields: fields.filter(f => f.type === 'negative').length,
luckFields: fields.filter(f => f.type === 'luck').length,
errorRate: validationResults.errorRate,
targetCount: Array.from(targetFieldsMap.values()).reduce((sum, targets) => sum + targets.length, 0)
});
return {
fields,
border,
validationResults: validationResults.validationResults,
totalErrorRate: validationResults.errorRate
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
};
}
@@ -52,6 +128,7 @@ export class BoardGenerationService {
luckFieldCount: number
): SpecialFieldInfo[] {
const totalSpecial = positiveFieldCount + negativeFieldCount + luckFieldCount;
<<<<<<< HEAD
const specialFields: SpecialFieldInfo[] = [];
// Generate unique random positions
@@ -63,6 +140,29 @@ export class BoardGenerationService {
// Convert to sorted array
const sortedPositions = Array.from(positions).sort((a, b) => a - b);
=======
const positions: number[] = [];
const specialFields: SpecialFieldInfo[] = [];
// Random placement with retry for good distribution
let attempts = 0;
while (positions.length < totalSpecial && attempts < 100) {
const position = Math.floor(Math.random() * 100) + 1; // 1-100
if (!positions.includes(position)) {
// Check minimum distance from existing positions
const tooClose = positions.some(existingPos => Math.abs(existingPos - position) < 3);
if (!tooClose || attempts > 50) { // Relax distance requirement after many attempts
positions.push(position);
}
}
attempts++;
}
// Sort positions and assign types
positions.sort((a, b) => a - b);
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
// Distribute types randomly
const types: ('positive' | 'negative' | 'luck')[] = [
@@ -77,7 +177,11 @@ export class BoardGenerationService {
[types[i], types[j]] = [types[j], types[i]];
}
<<<<<<< HEAD
sortedPositions.forEach((position, index) => {
=======
positions.forEach((position, index) => {
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
specialFields.push({
position,
type: types[index] || 'positive'
@@ -87,14 +191,156 @@ export class BoardGenerationService {
return specialFields;
}
<<<<<<< HEAD
private calculatePatternBasedStepValues(specialFields: SpecialFieldInfo[]): GameField[] {
=======
private selectTargetFields(specialFields: SpecialFieldInfo[]): Map<number, TargetField[]> {
const targetFieldsMap = new Map<number, TargetField[]>();
specialFields.forEach(field => {
if (field.type === 'luck') {
// Luck fields don't need target calculations
targetFieldsMap.set(field.position, []);
return;
}
const targets: TargetField[] = [];
const usedTargets = new Set<number>();
// Generate 6 different target fields (for dice 1-6) with 20-30 rule compliance
for (let i = 0; i < 6; i++) {
let targetField: number;
let distance: number;
let attempts = 0;
do {
// Determine max distance based on field position (20-30 rule)
let maxDistance: number;
let maxBackward: number;
if (field.position <= 85) {
maxDistance = 20;
maxBackward = 20;
} else {
maxDistance = 20; // forward
maxBackward = 30; // backward
}
// Create variety in distances within the allowed range
const distanceType = Math.random();
if (distanceType < 0.5) {
// Close distance (50% chance) - 1 to 1/3 of max
distance = Math.floor(Math.random() * Math.floor(maxDistance / 3)) + 1;
} else {
// Far distance (50% chance) - 1/3 to max
distance = Math.floor(Math.random() * (maxDistance - Math.floor(maxDistance / 3))) + Math.floor(maxDistance / 3);
}
// Randomly choose forward or backward
if (Math.random() < 0.5) {
distance = -Math.min(distance, maxBackward);
} else {
distance = Math.min(distance, maxDistance);
}
targetField = field.position + distance;
// Ensure target is within valid range
if (targetField < 1) targetField = 1;
if (targetField > 100) targetField = 100;
// Recalculate actual distance after clamping
distance = Math.abs(targetField - field.position);
attempts++;
} while (usedTargets.has(targetField) && attempts < 30);
if (!usedTargets.has(targetField)) {
usedTargets.add(targetField);
targets.push({
fieldNumber: targetField,
distance: Math.abs(targetField - field.position)
});
} else {
// Fallback: use a nearby valid target
let fallbackTarget = field.position + (i - 3); // Create some variety around current position
if (fallbackTarget < 1) fallbackTarget = 1;
if (fallbackTarget > 100) fallbackTarget = 100;
targets.push({
fieldNumber: fallbackTarget,
distance: Math.abs(fallbackTarget - field.position)
});
}
}
targetFieldsMap.set(field.position, targets);
});
return targetFieldsMap;
}
private createStrategicBorder(targetFieldsMap: Map<number, TargetField[]>): number[] {
// Collect all target field numbers
const targetNumbers = new Set<number>();
targetFieldsMap.forEach(targets => {
targets.forEach(target => targetNumbers.add(target.fieldNumber));
});
// Create array of all numbers 1-100
const allNumbers = Array.from({ length: 100 }, (_, i) => i + 1);
// Separate target numbers from remaining numbers
const remainingNumbers = allNumbers.filter(num => !targetNumbers.has(num));
// Shuffle remaining numbers
for (let i = remainingNumbers.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[remainingNumbers[i], remainingNumbers[j]] = [remainingNumbers[j], remainingNumbers[i]];
}
// Create border with strategic placement
const border: number[] = [];
const targetArray = Array.from(targetNumbers);
// Encourage overlap by placing target numbers first, then fill with random
let targetIndex = 0;
let remainingIndex = 0;
for (let i = 0; i < 100; i++) {
// Alternate between target numbers and remaining numbers, but favor targets when available
if (targetIndex < targetArray.length && (remainingIndex >= remainingNumbers.length || Math.random() < 0.6)) {
border.push(targetArray[targetIndex]);
targetIndex++;
} else if (remainingIndex < remainingNumbers.length) {
border.push(remainingNumbers[remainingIndex]);
remainingIndex++;
} else {
// Fallback - should not happen if logic is correct
border.push((i % 100) + 1);
}
}
return border;
}
private calculateStepValues(
specialFields: SpecialFieldInfo[],
targetFieldsMap: Map<number, TargetField[]>,
border: number[]
): GameField[] {
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
// Initialize all fields as regular
const fields: GameField[] = Array.from({ length: 100 }, (_, i) => ({
position: i + 1,
type: 'regular' as const
}));
<<<<<<< HEAD
// Update special fields with pattern-based step values
=======
// Update special fields with calculated step values
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
specialFields.forEach(specialField => {
const fieldIndex = specialField.position - 1; // Convert to 0-based index
fields[fieldIndex].type = specialField.type;
@@ -104,6 +350,7 @@ export class BoardGenerationService {
return;
}
<<<<<<< HEAD
// Calculate step values based on position rules
let maxStepValue: number;
let minStepValue: number;
@@ -127,12 +374,64 @@ export class BoardGenerationService {
// Negative fields: use negative step values (-3 to -8 range)
const stepValue = -(Math.floor(Math.random() * 6) + 3); // -3 to -8
fields[fieldIndex].stepValue = Math.max(stepValue, minStepValue);
=======
const targets = targetFieldsMap.get(specialField.position) || [];
if (targets.length === 0) return;
// NEW APPROACH: Calculate step value that will land on first target with dice=1
// This ensures we have a baseline that works, then dice 2-6 will hit other targets
const firstTarget = targets[0];
const targetIndexInBorder = border.indexOf(firstTarget.fieldNumber);
if (targetIndexInBorder !== -1) {
// Start from field position in border (field position = border index + 1, but we want 0-based)
const startBorderIndex = (specialField.position - 1) % border.length;
// Calculate step value needed to reach target with dice=1
let stepValue: number;
if (specialField.type === 'positive') {
// For positive: move right to target, then +1 more for dice=1
stepValue = targetIndexInBorder - startBorderIndex - 1; // -1 for dice offset
// Handle wrap-around
if (stepValue < 0) {
stepValue += border.length;
}
} else {
// For negative: move left to target, then -1 more for dice=1
stepValue = startBorderIndex - targetIndexInBorder + 1; // +1 for dice offset
// Handle wrap-around
if (stepValue > border.length) {
stepValue -= border.length;
}
// Make negative for negative fields
stepValue = -stepValue;
}
fields[fieldIndex].stepValue = stepValue;
// Debug logging for step value calculation
logOther(`Calculated step value for ${specialField.type} field at position ${specialField.position}`, {
targetField: firstTarget.fieldNumber,
targetIndexInBorder,
startBorderIndex,
calculatedStepValue: stepValue,
fieldType: specialField.type
});
} else {
// Fallback if target not found in border (shouldn't happen)
fields[fieldIndex].stepValue = specialField.type === 'positive' ? 1 : -1;
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
}
});
return fields;
}
<<<<<<< HEAD
// This method can be used by FieldEffectService for movement calculations
public calculatePatternBasedMovement(
currentPosition: number,
@@ -175,6 +474,89 @@ export class BoardGenerationService {
} else {
return 0; // Other even positions
}
=======
private validateBoardGeneration(fields: GameField[], border: number[]): {
validationResults: { [fieldIndex: number]: number[] };
errorRate: number;
} {
const validationResults: { [fieldIndex: number]: number[] } = {};
let totalCombinations = 0;
let invalidCombinations = 0;
fields.forEach((field, fieldIndex) => {
if (field.type !== 'positive' && field.type !== 'negative') {
return; // Skip non-special fields
}
const diceOutcomes: number[] = [];
for (let diceValue = 1; diceValue <= 6; diceValue++) {
totalCombinations++;
try {
const result = this.calculateBorderMovement(
field.position,
field.stepValue || 0,
diceValue,
border,
field.type === 'positive'
);
// Validate 20-30 rule
const distance = Math.abs(result - field.position);
const isValid = this.validate20_30Rule(field.position, result, distance);
if (isValid) {
diceOutcomes.push(result);
} else {
diceOutcomes.push(-1); // Mark as invalid
invalidCombinations++;
}
} catch (error) {
diceOutcomes.push(-1); // Mark as invalid
invalidCombinations++;
}
}
validationResults[fieldIndex] = diceOutcomes;
});
const errorRate = totalCombinations > 0 ? (invalidCombinations / totalCombinations) * 100 : 0;
return {
validationResults,
errorRate: Math.round(errorRate * 100) / 100 // Round to 2 decimal places
};
}
private calculateBorderMovement(
currentPosition: number,
stepValue: number,
diceValue: number,
border: number[],
isPositive: boolean
): number {
// Step 1: Find border index for current field (field position corresponds to border index)
let borderIndex = (currentPosition - 1) % border.length; // Convert to 0-based, handle wraparound
// Step 2: Apply field step value (handle negative step values for negative fields)
if (isPositive) {
borderIndex = (borderIndex + Math.abs(stepValue)) % border.length;
} else {
// For negative fields, stepValue is already negative, so we subtract it (which adds its absolute value)
borderIndex = (borderIndex - stepValue + border.length) % border.length;
}
// Step 3: Apply dice value
if (isPositive) {
borderIndex = (borderIndex + diceValue) % border.length;
} else {
borderIndex = (borderIndex - diceValue + border.length) % border.length;
}
// Step 4: Return the field number at final border position
return border[borderIndex];
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
}
private validate20_30Rule(currentPosition: number, targetPosition: number, distance: number): boolean {
@@ -196,4 +578,46 @@ export class BoardGenerationService {
return false;
}
<<<<<<< HEAD
=======
private generateFallbackBoard(
positiveFieldCount: number,
negativeFieldCount: number,
luckFieldCount: number
): BoardData {
// Simple fallback: create basic board with minimal special fields
const fields: GameField[] = Array.from({ length: 100 }, (_, i) => ({
position: i + 1,
type: 'regular' as const
}));
// Add a few special fields with safe step values
let specialCount = 0;
for (let i = 10; i < 90 && specialCount < positiveFieldCount + negativeFieldCount; i += 10) {
if (specialCount < positiveFieldCount) {
fields[i].type = 'positive';
fields[i].stepValue = 1;
} else {
fields[i].type = 'negative';
fields[i].stepValue = -1;
}
specialCount++;
}
// Simple border: shuffled 1-100
const border = Array.from({ length: 100 }, (_, i) => i + 1);
for (let i = border.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[border[i], border[j]] = [border[j], border[i]];
}
return {
fields,
border,
validationResults: {},
totalErrorRate: 100 // Mark as fallback
};
}
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
}
@@ -37,7 +37,11 @@ export class GenerateBoardCommandHandler {
);
const executionTime = Date.now() - startTime;
<<<<<<< HEAD
logOther(`Board generation completed for game ${cmd.gameId} in ${executionTime}ms using pattern-based approach`);
=======
logOther(`Board generation completed for game ${cmd.gameId} in ${executionTime}ms. Error rate: ${boardData.totalErrorRate}%`);
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
} catch (error) {
logError(`Board generation failed for game ${cmd.gameId}:`, error as Error);
@@ -46,6 +50,12 @@ export class GenerateBoardCommandHandler {
const errorData: BoardData = {
gameId: cmd.gameId,
fields: [],
<<<<<<< HEAD
=======
border: [],
validationResults: {},
totalErrorRate: 100,
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
generationComplete: false,
error: error instanceof Error ? error.message : 'Unknown error',
generatedAt: new Date()
@@ -151,6 +151,7 @@ export class JoinGameCommandHandler {
isOnline: true
};
<<<<<<< HEAD
// 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
@@ -160,6 +161,8 @@ export class JoinGameCommandHandler {
throw new Error(`Player name "${command.playerName}" is already in use in this game`);
}
=======
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
// Update players list (remove if exists, then add)
gameData.currentPlayers = gameData.currentPlayers.filter(p => p.playerId !== command.playerId);
gameData.currentPlayers.push(newPlayer);
@@ -170,6 +173,12 @@ export class JoinGameCommandHandler {
// Store updated data in Redis with TTL (24 hours)
await this.redisService.setWithExpiry(redisKey, JSON.stringify(gameData), 24 * 60 * 60);
<<<<<<< HEAD
=======
// Add player to active players set
await this.redisService.setAdd(`active_players:${game.id}`, command.playerId);
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
logOther('Game data updated in Redis', {
gameId: game.id,
gameCode: game.gamecode,
@@ -210,6 +219,10 @@ export class JoinGameCommandHandler {
gameData.currentPlayers = gameData.currentPlayers.filter(p => p.playerId !== playerId);
await this.redisService.setWithExpiry(redisKey, JSON.stringify(gameData), 24 * 60 * 60);
<<<<<<< HEAD
=======
await this.redisService.setRemove(`active_players:${gameId}`, playerId);
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
}
} catch (error) {
logError('Failed to remove player from Redis', error instanceof Error ? error : new Error(String(error)));
@@ -64,7 +64,11 @@ export class StartGameCommandHandler {
gamecode,
maxplayers: command.maxplayers,
logintype: command.logintype,
<<<<<<< HEAD
createdby: command.userid!,
=======
createdby: command.userid || null,
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
orgid: command.orgid || null,
gamedecks,
players: [],
@@ -28,7 +28,11 @@ export interface ActiveGamePlayData {
turnSequence: string[]; // Ordered array of player IDs based on turnOrder
websocketRoom: string;
gamePhase: 'starting' | 'playing' | 'paused' | 'finished';
<<<<<<< HEAD
boardData: BoardData; // Generated board with fields
=======
boardData: BoardData; // Generated board with fields and border
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
}
export interface GameStartResult {
@@ -362,7 +366,13 @@ export class StartGamePlayCommandHandler {
logOther(`Board data found for game ${gameId}`, {
generationComplete: boardData.generationComplete,
hasError: !!boardData.error,
<<<<<<< HEAD
fieldsCount: boardData.fields ? boardData.fields.length : 0
=======
fieldsCount: boardData.fields ? boardData.fields.length : 0,
borderLength: boardData.border ? boardData.border.length : 0,
totalErrorRate: boardData.totalErrorRate
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
});
if (boardData.generationComplete) {
@@ -372,7 +382,13 @@ export class StartGamePlayCommandHandler {
}
logOther(`Board generation completed for game ${gameId}`, {
<<<<<<< HEAD
fieldCount: boardData.fields.length,
=======
errorRate: boardData.totalErrorRate,
fieldCount: boardData.fields.length,
borderLength: boardData.border.length,
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
waitTime: Date.now() - startTime
});
@@ -79,7 +79,11 @@ export async function authRequired(req: Request, res: Response, next: NextFuncti
orgId: payload.orgId
}, req);
<<<<<<< HEAD
const refreshed = jwtService.refreshIfNeeded(payload, res, req);
=======
const refreshed = jwtService.refreshIfNeeded(payload, res);
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
if (refreshed) {
logAuth('Token refreshed', payload.userId, undefined, req);
}
@@ -132,7 +136,11 @@ export async function adminRequired(req: Request, res: Response, next: NextFunct
orgId: payload.orgId
}, req);
<<<<<<< HEAD
const refreshed = jwtService.refreshIfNeeded(payload, res, req);
=======
const refreshed = jwtService.refreshIfNeeded(payload, res);
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
if (refreshed) {
logAuth('Admin token refreshed', payload.userId, undefined, req);
}
@@ -60,9 +60,12 @@ import { EmailService } from './EmailService';
import { GameTokenService } from './GameTokenService';
import { ContactEmailService } from './ContactEmailService';
import { DeckImportExportService } from './DeckImportExportService';
<<<<<<< HEAD
import { FieldEffectService } from './FieldEffectService';
import { CardDrawingService } from './CardDrawingService';
import { GamemasterService } from './GamemasterService';
=======
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
import { RedisService } from './RedisService';
import { GameService } from '../Game/GameService';
import { BoardGenerationService } from '../Game/BoardGenerationService';
@@ -90,9 +93,12 @@ export class DIContainer {
private _gameTokenService: GameTokenService | null = null;
private _contactEmailService: ContactEmailService | null = null;
private _deckImportExportService: DeckImportExportService | null = null;
<<<<<<< HEAD
private _cardDrawingService: CardDrawingService | null = null;
private _gamemasterService: GamemasterService | null = null;
private _fieldEffectService: FieldEffectService | null = null;
=======
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
private _gameService: GameService | null = null;
private _boardGenerationService: BoardGenerationService | null = null;
@@ -232,6 +238,7 @@ export class DIContainer {
return this._deckImportExportService;
}
<<<<<<< HEAD
public get cardDrawingService(): CardDrawingService {
if (!this._cardDrawingService) {
this._cardDrawingService = new CardDrawingService();
@@ -256,6 +263,8 @@ export class DIContainer {
return this._fieldEffectService;
}
=======
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
public get gameService(): GameService {
if (!this._gameService) {
this._gameService = new GameService();
@@ -54,6 +54,31 @@ interface DeleteMessageData {
messageId: string;
}
// Game-related WebSocket interfaces (prepared for future implementation)
interface JoinGameRoomData {
gameCode: string;
}
interface LeaveGameRoomData {
gameCode: string;
}
interface GameStateUpdateData {
gameId: string;
gameCode: string;
players: string[];
state: string;
currentTurn?: string;
}
interface GameActionData {
gameId: string;
gameCode: string;
playerId: string;
action: 'pick_card' | 'play_card' | 'end_turn' | 'leave_game';
data?: any;
}
export class WebSocketService {
private io: SocketIOServer;
private jwtService: JWTService;
@@ -237,6 +262,14 @@ export class WebSocketService {
socket.on('chat:archive:delete', (data: DeleteChatArchiveData) => this.handleDeleteChatArchive(socket, data));
socket.on('message:delete', (data: DeleteMessageData) => this.handleDeleteMessage(socket, data));
<<<<<<< HEAD
=======
// Game event handlers (prepared for future implementation)
socket.on('game:join', (data: JoinGameRoomData) => this.handleJoinGameRoom(socket, data));
socket.on('game:leave', (data: LeaveGameRoomData) => this.handleLeaveGameRoom(socket, data));
socket.on('game:action', (data: GameActionData) => this.handleGameAction(socket, data));
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
socket.on('disconnect', () => this.handleDisconnection(socket));
}
@@ -1173,4 +1206,211 @@ export class WebSocketService {
}
}
}
// Game-related WebSocket handlers (prepared for future implementation)
/**
* Handle player joining a game room for real-time updates
* @param socket The authenticated socket
* @param data Game room data containing game code
*/
private async handleJoinGameRoom(socket: AuthenticatedSocket, data: JoinGameRoomData) {
try {
const userId = socket.userId!;
const gameRoom = `game_${data.gameCode}`;
logAuth('Player joining game room', userId, {
gameCode: data.gameCode,
gameRoom,
socketId: socket.id
});
// Join the WebSocket room for this game
await socket.join(gameRoom);
// Emit confirmation to the player
socket.emit('game:joined', {
gameCode: data.gameCode,
room: gameRoom,
message: 'Successfully joined game room'
});
// Notify other players in the game room
socket.to(gameRoom).emit('game:player_joined', {
playerId: userId,
gameCode: data.gameCode,
timestamp: new Date().toISOString()
});
logAuth('Player joined game room successfully', userId, {
gameCode: data.gameCode,
gameRoom
});
} catch (error) {
logError('Error joining game room', error as Error);
socket.emit('game:error', {
message: 'Failed to join game room',
gameCode: data.gameCode
});
}
}
/**
* Handle player leaving a game room
* @param socket The authenticated socket
* @param data Game room data containing game code
*/
private async handleLeaveGameRoom(socket: AuthenticatedSocket, data: LeaveGameRoomData) {
try {
const userId = socket.userId!;
const gameRoom = `game_${data.gameCode}`;
logAuth('Player leaving game room', userId, {
gameCode: data.gameCode,
gameRoom,
socketId: socket.id
});
// Leave the WebSocket room
await socket.leave(gameRoom);
// Notify other players in the game room
socket.to(gameRoom).emit('game:player_left', {
playerId: userId,
gameCode: data.gameCode,
timestamp: new Date().toISOString()
});
// Confirm to the leaving player
socket.emit('game:left', {
gameCode: data.gameCode,
message: 'Successfully left game room'
});
logAuth('Player left game room successfully', userId, {
gameCode: data.gameCode,
gameRoom
});
} catch (error) {
logError('Error leaving game room', error as Error);
socket.emit('game:error', {
message: 'Failed to leave game room',
gameCode: data.gameCode
});
}
}
/**
* Handle game actions (cards, turns, etc.) - prepared for future implementation
* @param socket The authenticated socket
* @param data Game action data
*/
private async handleGameAction(socket: AuthenticatedSocket, data: GameActionData) {
try {
const userId = socket.userId!;
const gameRoom = `game_${data.gameCode}`;
logAuth('Game action received', userId, {
gameId: data.gameId,
gameCode: data.gameCode,
action: data.action,
socketId: socket.id
});
// Validate that the player is authorized to perform this action
if (data.playerId !== userId) {
socket.emit('game:error', {
message: 'Unauthorized action',
gameCode: data.gameCode
});
return;
}
// TODO: Implement specific game logic here
// This will be implemented when the game flow is discussed
// For now, just broadcast the action to other players
socket.to(gameRoom).emit('game:action_performed', {
playerId: userId,
gameCode: data.gameCode,
action: data.action,
data: data.data,
timestamp: new Date().toISOString()
});
// Confirm action to the acting player
socket.emit('game:action_confirmed', {
gameCode: data.gameCode,
action: data.action,
message: 'Action processed successfully'
});
logAuth('Game action processed', userId, {
gameId: data.gameId,
gameCode: data.gameCode,
action: data.action
});
} catch (error) {
logError('Error processing game action', error as Error);
socket.emit('game:error', {
message: 'Failed to process game action',
gameCode: data.gameCode,
action: data.action
});
}
}
/**
* Broadcast game state updates to all players in a game
* @param gameCode The game code
* @param gameState The updated game state
*/
public broadcastGameStateUpdate(gameCode: string, gameState: GameStateUpdateData): void {
try {
const gameRoom = `game_${gameCode}`;
this.io.to(gameRoom).emit('game:state_updated', {
...gameState,
timestamp: new Date().toISOString()
});
logRequest('Game state broadcasted', undefined, undefined, {
gameCode,
gameRoom,
playerCount: gameState.players.length
});
} catch (error) {
logError('Error broadcasting game state', error as Error);
}
}
/**
* Notify players when a game starts
* @param gameCode The game code
* @param players Array of player IDs
*/
public notifyGameStart(gameCode: string, players: string[]): void {
try {
const gameRoom = `game_${gameCode}`;
this.io.to(gameRoom).emit('game:started', {
gameCode,
players,
message: 'Game has started!',
timestamp: new Date().toISOString()
});
logRequest('Game start notification sent', undefined, undefined, {
gameCode,
playerCount: players.length
});
} catch (error) {
logError('Error notifying game start', error as Error);
}
}
}
@@ -17,6 +17,7 @@ export class LogoutCommandHandler {
try {
logAuth('Logout process started', userId);
<<<<<<< HEAD
// 1. Get tokens from request to blacklist them
let accessTokenToBlacklist: string | null = null;
let refreshTokenToBlacklist: string | null = null;
@@ -41,10 +42,32 @@ export class LogoutCommandHandler {
// 2. Blacklist both access and refresh tokens in Redis
if (accessTokenToBlacklist && req) {
try {
=======
// 1. Get token from request to blacklist it
let tokenToBlacklist: 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) {
const authHeader = req.headers.authorization;
if (authHeader.startsWith('Bearer ')) {
tokenToBlacklist = authHeader.substring(7);
}
}
}
// 2. Blacklist the current JWT token in Redis (if available)
if (tokenToBlacklist && req) {
try {
// Store token in blacklist with expiration matching token expiry
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
const decoded = this.jwtService.verify(req);
if (decoded && decoded.exp) {
const ttl = decoded.exp - Math.floor(Date.now() / 1000);
if (ttl > 0) {
<<<<<<< HEAD
await this.redisService.setWithExpiry(`blacklist:${accessTokenToBlacklist}`, 'true', ttl);
logAuth('Access token blacklisted', userId, { tokenExpiry: ttl });
}
@@ -74,6 +97,24 @@ export class LogoutCommandHandler {
if (req) {
this.jwtService.logout(req, res);
}
=======
await this.redisService.setWithExpiry(`blacklist:${tokenToBlacklist}`, 'true', ttl);
logAuth('JWT token blacklisted', userId, { tokenExpiry: ttl });
}
}
} catch (error) {
logWarning('Failed to blacklist 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: '/'
});
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
// 4. Remove user from active sessions in Redis
try {
@@ -31,7 +31,13 @@ export enum ConsequenceType {
MOVE_BACKWARD = 1,
LOSE_TURN = 2,
EXTRA_TURN = 3,
<<<<<<< HEAD
GO_TO_START = 5
=======
SWAP_POSITION = 4,
GO_TO_START = 5,
TURN_AGAIN = 6
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
}
export interface Consequence {
@@ -1,5 +1,9 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
<<<<<<< HEAD
import { Consequence, CardType } from '../Deck/DeckAggregate';
=======
import { Consequence } from '../Deck/DeckAggregate';
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
export enum GameState {
WAITING = 0,
@@ -23,8 +27,12 @@ export enum DeckType {
export interface GameCard {
cardid: string;
question?: string;
<<<<<<< HEAD
answer?: any; // Support complex answer structures (string, object, array)
type?: CardType; // Card type for validation logic
=======
answer?: string;
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
consequence?: Consequence | null;
played?: boolean;
playerid?: string;
@@ -50,6 +58,7 @@ export class GameAggregate {
@Column({ type: 'int', default: LoginType.PUBLIC })
logintype!: LoginType;
<<<<<<< HEAD
@Column({ type: 'int', default: 50 })
boardsize!: number;
@@ -63,6 +72,18 @@ export class GameAggregate {
gamedecks!: GameDeck[];
@Column({ type: 'uuid', array: true, default: () => "'{}'", name: 'playerids' })
=======
@Column({ type: 'varchar', length: 255, nullable: true })
createdby!: string | null;
@Column({ type: 'varchar', length: 255, nullable: true })
orgid!: string | null;
@Column({ type: 'json' })
gamedecks!: GameDeck[];
@Column({ type: 'json', default: () => "'[]'" })
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
players!: string[];
@Column({ type: 'boolean', default: false })
@@ -71,22 +92,37 @@ export class GameAggregate {
@Column({ type: 'boolean', default: false })
finished!: boolean;
<<<<<<< HEAD
@Column({ type: 'uuid', nullable: true, name: 'winnerid' })
=======
@Column({ type: 'varchar', length: 255, nullable: true })
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
winner!: string | null;
@Column({ type: 'int', default: GameState.WAITING })
state!: GameState;
<<<<<<< HEAD
@CreateDateColumn({ name: 'createDate' })
=======
@CreateDateColumn({ name: 'create_date' })
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
createdate!: Date;
@Column({ type: 'timestamp', nullable: true, name: 'start_date' })
startdate!: Date | null;
<<<<<<< HEAD
@Column({ type: 'timestamp', nullable: true, name: 'finishDate' })
enddate!: Date | null;
@UpdateDateColumn({ name: 'updateDate' })
=======
@Column({ type: 'timestamp', nullable: true, name: 'end_date' })
enddate!: Date | null;
@UpdateDateColumn({ name: 'update_date' })
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
updatedate!: Date;
}
@@ -100,6 +136,12 @@ export interface GameField {
export interface BoardData {
gameId?: string;
fields: GameField[];
<<<<<<< HEAD
=======
border: number[];
validationResults: { [fieldIndex: number]: number[] };
totalErrorRate: number;
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
generationComplete?: boolean;
generatedAt?: Date;
error?: string;
@@ -1,9 +1,27 @@
import { GameAggregate } from '../Game/GameAggregate';
<<<<<<< HEAD
import { IPaginatedRepository } from './IBaseRepository';
export interface IGameRepository extends IPaginatedRepository<GameAggregate, { games: GameAggregate[], totalCount: number }> {
// Game-specific methods
findByGameCode(gamecode: string): Promise<GameAggregate | null>;
=======
export interface IGameRepository {
create(game: Partial<GameAggregate>): Promise<GameAggregate>;
findByPage(from: number, to: number): Promise<{ games: GameAggregate[], totalCount: number }>;
findByPageIncludingDeleted(from: number, to: number): Promise<{ games: GameAggregate[], totalCount: number }>;
findById(id: string): Promise<GameAggregate | null>;
findByIdIncludingDeleted(id: string): Promise<GameAggregate | null>;
findByGameCode(gamecode: string): Promise<GameAggregate | null>;
search(query: string, limit?: number, offset?: number): Promise<{ games: GameAggregate[], totalCount: number }>;
searchIncludingDeleted(query: string, limit?: number, offset?: number): Promise<{ games: GameAggregate[], totalCount: number }>;
update(id: string, update: Partial<GameAggregate>): Promise<GameAggregate | null>;
delete(id: string): Promise<any>;
softDelete(id: string): Promise<GameAggregate | null>;
// Game-specific methods
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
findActiveGames(): Promise<GameAggregate[]>;
findGamesByPlayer(playerId: string): Promise<GameAggregate[]>;
findWaitingGames(): Promise<GameAggregate[]>;
@@ -0,0 +1,28 @@
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,15 @@
import { MigrationInterface, QueryRunner } from "typeorm";
<<<<<<<< HEAD:SerpentRace_Backend/src/Infrastructure/Migrationsettings/1758463928499-full.ts
export class Full1758463928499 implements MigrationInterface {
========
export class Full1757939815062 implements MigrationInterface {
>>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2:SerpentRace_Backend/src/Infrastructure/Migrationsettings/1757939815062-full.ts
public async up(queryRunner: QueryRunner): Promise<void> {
}
public async down(queryRunner: QueryRunner): Promise<void> {
}
}
@@ -1,6 +1,10 @@
import { MigrationInterface, QueryRunner } from "typeorm";
<<<<<<<< HEAD:SerpentRace_Backend/src/Infrastructure/Migrationsettings/1758463928499-full.ts
export class Full1758463928499 implements MigrationInterface {
========
export class Full1757939815062 implements MigrationInterface {
>>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2:SerpentRace_Backend/src/Infrastructure/Migrationsettings/1757939815062-full.ts
public async up(queryRunner: QueryRunner): Promise<void> {
}