bc/server/bc.js

327 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// /server/bc.js - Главный файл сервера Battle Club
require('dotenv').config({ path: require('node:path').resolve(process.cwd(), '.env') });
const express = require('express');
const http = require('http');
const { Server } = require('socket.io');
const path = require('path');
const jwt = require('jsonwebtoken');
const cors = require('cors');
const authService = require('./auth/authService');
const GameManager = require('./game/GameManager');
const db = require('./core/db'); // Используется для auth, не для игр в этом варианте
const GAME_CONFIG = require('./core/config');
const app = express();
const server = http.createServer(app);
// --- НАСТРОЙКА EXPRESS ---
console.log(`[BC.JS CONFIG] Reading environment variables for Express CORS...`);
console.log(`[BC.JS CONFIG] NODE_ENV: ${process.env.NODE_ENV}`);
console.log(`[BC.JS CONFIG] process.env.CORS_ORIGIN_CLIENT: ${process.env.CORS_ORIGIN_CLIENT}`);
const clientOrigin = process.env.CORS_ORIGIN_CLIENT || (process.env.NODE_ENV === 'development' ? '*' : undefined);
console.log(`[BC.JS CONFIG] Effective clientOrigin for HTTP CORS: ${clientOrigin === '*' ? "'*'" : clientOrigin || 'NOT SET (CORS will likely fail if not development)'}`);
if (!clientOrigin && process.env.NODE_ENV !== 'development' && process.env.NODE_ENV !== undefined) {
console.warn("[BC.JS CONFIG WARNING] CORS_ORIGIN_CLIENT is not set for a non-development and non-undefined NODE_ENV. HTTP API requests from browsers might be blocked by CORS.");
}
app.use(cors({
origin: clientOrigin,
methods: ["GET", "POST"],
credentials: true
}));
app.use(express.json());
const publicPath = path.join(__dirname, '..', 'public');
console.log(`[BC.JS CONFIG] Serving static files from: ${publicPath}`);
app.use(express.static(publicPath));
// --- HTTP МАРШРУТЫ АУТЕНТИФИКАЦИИ ---
app.post('/auth/register', async (req, res) => {
const { username, password } = req.body;
console.log(`[BC HTTP /auth/register] Attempt for username: "${username}" from IP: ${req.ip}. Origin: ${req.headers.origin}`);
if (!username || !password) {
console.warn('[BC HTTP /auth/register] Bad request: Username or password missing.');
return res.status(400).json({ success: false, message: 'Имя пользователя и пароль обязательны.' });
}
const result = await authService.registerUser(username, password);
if (result.success) {
console.log(`[BC HTTP /auth/register] Success for "${username}".`);
res.status(201).json(result);
} else {
console.warn(`[BC HTTP /auth/register] Failed for "${username}": ${result.message}`);
res.status(400).json(result);
}
});
app.post('/auth/login', async (req, res) => {
const { username, password } = req.body;
console.log(`[BC HTTP /auth/login] Attempt for username: "${username}" from IP: ${req.ip}. Origin: ${req.headers.origin}`);
if (!username || !password) {
console.warn('[BC HTTP /auth/login] Bad request: Username or password missing.');
return res.status(400).json({ success: false, message: 'Имя пользователя и пароль обязательны.' });
}
const result = await authService.loginUser(username, password);
if (result.success) {
console.log(`[BC HTTP /auth/login] Success for "${username}".`);
res.json(result);
} else {
console.warn(`[BC HTTP /auth/login] Failed for "${username}": ${result.message}`);
res.status(401).json(result);
}
});
// --- НАСТРОЙКА SOCKET.IO ---
console.log(`[BC.JS CONFIG] Reading environment variables for Socket.IO CORS...`);
console.log(`[BC.JS CONFIG] process.env.CORS_ORIGIN_SOCKET: ${process.env.CORS_ORIGIN_SOCKET}`);
const socketCorsOrigin = process.env.CORS_ORIGIN_SOCKET || (process.env.NODE_ENV === 'development' ? '*' : undefined);
console.log(`[BC.JS CONFIG] Effective socketCorsOrigin for Socket.IO CORS: ${socketCorsOrigin === '*' ? "'*'" : socketCorsOrigin || 'NOT SET (Socket.IO CORS will likely fail if not development)'}`);
if (!socketCorsOrigin && process.env.NODE_ENV !== 'development' && process.env.NODE_ENV !== undefined) {
console.warn("[BC.JS CONFIG WARNING] CORS_ORIGIN_SOCKET is not set for a non-development and non-undefined NODE_ENV. Socket.IO connections from browsers might be blocked by CORS.");
}
const io = new Server(server, {
path: '/socket.io/',
cors: {
origin: socketCorsOrigin,
methods: ["GET", "POST"],
credentials: true
},
});
console.log(`[BC.JS CONFIG] Socket.IO server configured with path: ${io.path()} and effective CORS origin: ${io.opts.cors.origin === '*' ? "'*'" : io.opts.cors.origin || 'NOT SET'}`);
const gameManager = new GameManager(io);
const loggedInUsersBySocketId = {};
// --- MIDDLEWARE АУТЕНТИФИКАЦИИ SOCKET.IO ---
io.use(async (socket, next) => {
const token = socket.handshake.auth.token;
const clientIp = socket.handshake.headers['x-forwarded-for']?.split(',')[0].trim() || socket.handshake.address;
const originHeader = socket.handshake.headers.origin;
const socketPath = socket.nsp.name;
console.log(`[BC Socket.IO Middleware] Auth attempt for socket ${socket.id} from IP ${clientIp}. Token ${token ? 'present' : 'absent'}. Origin: ${originHeader}. Path: ${socketPath}`);
if (token) {
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
socket.userData = { userId: decoded.userId, username: decoded.username };
console.log(`[BC Socket.IO Middleware] Socket ${socket.id} authenticated for user ${decoded.username} (ID: ${decoded.userId}).`);
return next();
} catch (err) {
console.warn(`[BC Socket.IO Middleware] Socket ${socket.id} auth failed: Invalid token. Error: ${err.message}. Proceeding as unauthenticated.`);
}
} else {
console.log(`[BC Socket.IO Middleware] Socket ${socket.id} has no token. Proceeding as unauthenticated.`);
}
next();
});
// --- ОБРАБОТЧИКИ СОБЫТИЙ SOCKET.IO ---
io.on('connection', (socket) => {
const clientIp = socket.handshake.headers['x-forwarded-for']?.split(',')[0].trim() || socket.handshake.address;
const originHeader = socket.handshake.headers.origin;
const socketPath = socket.nsp.name;
if (socket.userData && socket.userData.userId) {
console.log(`[BC Socket.IO Connection] Authenticated user ${socket.userData.username} (ID: ${socket.userData.userId}) connected. Socket: ${socket.id}, IP: ${clientIp}, Origin: ${originHeader}, Path: ${socketPath}`);
loggedInUsersBySocketId[socket.id] = socket.userData;
// --- НАЧАЛО ИЗМЕНЕНИЯ ---
// Отправляем текущий список доступных PvP игр этому конкретному сокету
// после успешной аутентификации.
if (gameManager && typeof gameManager.getAvailablePvPGamesListForClient === 'function') {
console.log(`[BC Socket.IO Connection] Sending initial available PvP games list to authenticated user ${socket.userData.username} (Socket: ${socket.id})`);
const availableGames = gameManager.getAvailablePvPGamesListForClient();
socket.emit('availablePvPGamesList', availableGames);
} else {
console.error("[BC Socket.IO Connection] CRITICAL: gameManager or getAvailablePvPGamesListForClient not found for sending initial list!");
}
// --- КОНЕЦ ИЗМЕНЕНИЯ ---
if (gameManager && typeof gameManager.handleRequestGameState === 'function') {
gameManager.handleRequestGameState(socket, socket.userData.userId);
} else {
console.error("[BC Socket.IO Connection] CRITICAL: gameManager or handleRequestGameState not found for authenticated user!");
}
} else {
console.log(`[BC Socket.IO Connection] Unauthenticated user connected. Socket: ${socket.id}, IP: ${clientIp}, Origin: ${originHeader}, Path: ${socketPath}.`);
// --- НАЧАЛО ИЗМЕНЕНИЯ (опционально, если неаутентифицированные тоже видят список) ---
// Если неаутентифицированные пользователи тоже должны видеть список игр
/*
if (gameManager && typeof gameManager.getAvailablePvPGamesListForClient === 'function') {
console.log(`[BC Socket.IO Connection] Sending initial available PvP games list to unauthenticated socket ${socket.id}`);
const availableGames = gameManager.getAvailablePvPGamesListForClient();
socket.emit('availablePvPGamesList', availableGames);
} else {
console.error("[BC Socket.IO Connection] CRITICAL: gameManager or getAvailablePvPGamesListForClient not found for sending initial list to unauth user!");
}
*/
// --- КОНЕЦ ИЗМЕНЕНИЯ ---
}
socket.on('logout', () => {
const username = socket.userData?.username || 'UnknownUserOnLogout';
const userId = socket.userData?.userId;
console.log(`[BC Socket.IO 'logout' event] User: ${username} (ID: ${userId || 'N/A'}, Socket: ${socket.id}).`);
if (loggedInUsersBySocketId[socket.id]) {
delete loggedInUsersBySocketId[socket.id];
}
socket.userData = null;
console.log(`[BC Socket.IO 'logout' event] Session data for socket ${socket.id} cleared on server.`);
});
socket.on('playerSurrender', () => {
if (!socket.userData?.userId) {
console.warn(`[BC Socket.IO 'playerSurrender'] Denied for unauthenticated socket ${socket.id}.`);
socket.emit('gameError', { message: 'Необходимо войти в систему, чтобы сдаться в игре.' });
return;
}
const identifier = socket.userData.userId;
const username = socket.userData.username;
console.log(`[BC Socket.IO 'playerSurrender'] Request from user ${username} (ID: ${identifier}, Socket: ${socket.id})`);
if (gameManager && typeof gameManager.handlePlayerSurrender === 'function') {
gameManager.handlePlayerSurrender(identifier);
} else {
console.error("[BC Socket.IO 'playerSurrender'] CRITICAL: gameManager or handlePlayerSurrender method not found!");
socket.emit('gameError', { message: 'Ошибка сервера при обработке сдачи игры.' });
}
});
socket.on('leaveAiGame', () => {
if (!socket.userData?.userId) {
console.warn(`[BC Socket.IO 'leaveAiGame'] Denied for unauthenticated socket ${socket.id}.`);
socket.emit('gameError', { message: 'Необходимо войти в систему, чтобы покинуть AI игру.' });
return;
}
const identifier = socket.userData.userId;
const username = socket.userData.username;
console.log(`[BC Socket.IO 'leaveAiGame'] Request from user ${username} (ID: ${identifier}, Socket: ${socket.id})`);
if (gameManager && typeof gameManager.handleLeaveAiGame === 'function') {
gameManager.handleLeaveAiGame(identifier);
} else {
console.error("[BC Socket.IO 'leaveAiGame'] CRITICAL: gameManager or handleLeaveAiGame method not found!");
socket.emit('gameError', { message: 'Ошибка сервера при выходе из AI игры.' });
}
});
socket.on('createGame', (data) => {
if (!socket.userData?.userId) {
console.warn(`[BC Socket.IO 'createGame'] Denied for unauthenticated socket ${socket.id}.`);
socket.emit('gameError', { message: 'Необходимо войти в систему для создания игры.' });
return;
}
const identifier = socket.userData.userId;
const mode = data?.mode || 'ai';
const charKey = data?.characterKey;
console.log(`[BC Socket.IO 'createGame'] Request from ${socket.userData.username} (ID: ${identifier}). Mode: ${mode}, Char: ${charKey}`);
gameManager.createGame(socket, mode, charKey, identifier);
});
socket.on('joinGame', (data) => {
if (!socket.userData?.userId) {
console.warn(`[BC Socket.IO 'joinGame'] Denied for unauthenticated socket ${socket.id}.`);
socket.emit('gameError', { message: 'Необходимо войти для присоединения к PvP игре.' });
return;
}
const gameId = data?.gameId;
const userId = socket.userData.userId;
console.log(`[BC Socket.IO 'joinGame'] Request from ${socket.userData.username} (ID: ${userId}). GameID: ${gameId}`);
gameManager.joinGame(socket, gameId, userId);
});
socket.on('findRandomGame', (data) => {
if (!socket.userData?.userId) {
console.warn(`[BC Socket.IO 'findRandomGame'] Denied for unauthenticated socket ${socket.id}.`);
socket.emit('gameError', { message: 'Необходимо войти для поиска случайной PvP игры.' });
return;
}
const userId = socket.userData.userId;
const charKey = data?.characterKey;
console.log(`[BC Socket.IO 'findRandomGame'] Request from ${socket.userData.username} (ID: ${userId}). PrefChar: ${charKey}`);
gameManager.findAndJoinRandomPvPGame(socket, charKey, userId);
});
socket.on('requestPvPGameList', () => {
console.log(`[BC Socket.IO 'requestPvPGameList'] Request from socket ${socket.id} (User: ${socket.userData?.username || 'Unauth'}).`);
if (gameManager && typeof gameManager.getAvailablePvPGamesListForClient === 'function') {
const availableGames = gameManager.getAvailablePvPGamesListForClient();
socket.emit('availablePvPGamesList', availableGames);
} else {
console.error("[BC Socket.IO 'requestPvPGameList'] CRITICAL: gameManager or getAvailablePvPGamesListForClient not found!");
socket.emit('availablePvPGamesList', []); // Отправляем пустой список в случае ошибки
}
});
socket.on('requestGameState', () => {
if (!socket.userData?.userId) {
console.warn(`[BC Socket.IO 'requestGameState'] Denied for unauthenticated socket ${socket.id}.`);
socket.emit('gameNotFound', { message: 'Необходимо войти для восстановления игры.' });
return;
}
const userId = socket.userData.userId;
console.log(`[BC Socket.IO 'requestGameState'] Request from ${socket.userData.username} (ID: ${userId}, Socket: ${socket.id})`);
gameManager.handleRequestGameState(socket, userId);
});
socket.on('playerAction', (actionData) => {
if (!socket.userData?.userId) {
console.warn(`[BC Socket.IO 'playerAction'] Denied for unauthenticated socket ${socket.id}. Action: ${actionData?.actionType}`);
socket.emit('gameError', { message: 'Действие не разрешено: пользователь не аутентифицирован.' });
return;
}
const identifier = socket.userData.userId;
console.log(`[BC Socket.IO 'playerAction'] Action from ${socket.userData.username} (ID: ${identifier}). Type: ${actionData?.actionType}, Details: ${JSON.stringify(actionData)}`);
gameManager.handlePlayerAction(identifier, actionData);
});
socket.on('disconnect', (reason) => {
const identifier = socket.userData?.userId;
const username = socket.userData?.username || loggedInUsersBySocketId[socket.id]?.username || 'UnauthenticatedOrUnknown';
console.log(`[BC Socket.IO Disconnect] User ${username} (ID: ${identifier || 'N/A'}, Socket: ${socket.id}) disconnected. Reason: ${reason}.`);
if (identifier) {
gameManager.handleDisconnect(socket.id, identifier);
}
if (loggedInUsersBySocketId[socket.id]) {
delete loggedInUsersBySocketId[socket.id];
}
});
});
// --- ЗАПУСК СЕРВЕРА ---
const PORT = parseInt(process.env.BC_APP_PORT || '3200', 10);
const HOSTNAME = process.env.BC_APP_HOSTNAME || '127.0.0.1';
if (isNaN(PORT)) {
console.error(`[BC Server FATAL] Invalid BC_APP_PORT: "${process.env.BC_APP_PORT}". Expected a number.`);
process.exit(1);
}
server.listen(PORT, HOSTNAME, () => {
console.log(`[BC Server Startup] Battle Club HTTP Application Server running at http://${HOSTNAME}:${PORT}`);
if (HOSTNAME === '127.0.0.1') {
console.log(`[BC Server Startup] Server is listening on localhost only.`);
} else if (HOSTNAME === '0.0.0.0') {
console.log(`[BC Server Startup] Server is listening on all available network interfaces.`);
} else {
console.log(`[BC Server Startup] Server is listening on a specific interface: ${HOSTNAME}.`);
}
console.log(`[BC Server Startup] Static files served from: ${publicPath}`);
console.log(`[BC.JS Startup] Socket.IO server effective path: ${io.path()}`);
console.log(`[BC.JS Startup] HTTP API effective CORS origin: ${clientOrigin === '*' ? "'*'" : clientOrigin || 'NOT SET'}`);
console.log(`[BC.JS Startup] Socket.IO effective CORS origin: ${io.opts.cors.origin === '*' ? "'*'" : io.opts.cors.origin || 'NOT SET'}`);
});
process.on('unhandledRejection', (reason, promise) => {
console.error('[BC Server FATAL UnhandledRejection] Reason:', reason, 'Promise:', promise);
});
process.on('uncaughtException', (err) => {
console.error('[BC Server FATAL UncaughtException] Error:', err);
process.exit(1);
});