Merge remote-tracking branch 'origin/main'
This commit is contained in:
@@ -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> {
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user