// /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'); const GAME_CONFIG = require('./core/config'); const app = express(); const server = http.createServer(app); // --- НАСТРОЙКА EXPRESS --- const clientOrigin = process.env.CORS_ORIGIN_CLIENT || (process.env.NODE_ENV === 'development' ? '*' : undefined); console.log(`[BC Server] NODE_ENV: ${process.env.NODE_ENV}`); console.log(`[BC Server] process.env.CORS_ORIGIN_CLIENT: ${process.env.CORS_ORIGIN_CLIENT}`); console.log(`[BC Server] Calculated HTTP CORS Origin (clientOrigin): ${clientOrigin === '*' ? "'*'" : clientOrigin || 'Not explicitly set (will likely fail if not development)'}`); if (!clientOrigin && process.env.NODE_ENV !== 'development') { console.warn("[BC Server Config] CORS_ORIGIN_CLIENT не установлен для не-development сборки. HTTP API могут быть недоступны."); } app.use(cors({ origin: clientOrigin, methods: ["GET", "POST"], credentials: true })); app.use(express.json()); const publicPath = path.join(__dirname, '..', 'public'); console.log(`[BC Server] 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}`); 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 header: ${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 --- const socketCorsOrigin = process.env.CORS_ORIGIN_SOCKET || (process.env.NODE_ENV === 'development' ? '*' : undefined); console.log(`[BC Server] process.env.CORS_ORIGIN_SOCKET: ${process.env.CORS_ORIGIN_SOCKET}`); console.log(`[BC Server] Calculated Socket.IO CORS Origin (socketCorsOrigin): ${socketCorsOrigin === '*' ? "'*'" : socketCorsOrigin || 'Not explicitly set (will likely fail if not development)'}`); if (!socketCorsOrigin && process.env.NODE_ENV !== 'development') { console.warn("[BC Server Config] CORS_ORIGIN_SOCKET не установлен для не-development сборки. Socket.IO может быть недоступен."); } const io = new Server(server, { // ВАЖНО: Этот path должен соответствовать тому, как клиент подключается // и как прокси настроен (особенно stripPrefix для /socket.io). // Если клиент и прокси работают с /socket.io/ и прокси НЕ отрезает этот префикс // (stripPrefix: false для /socket.io в config.json), то здесь ДОЛЖЕН быть path. path: '/socket.io/', // <--- УБЕДИТЕСЬ, ЧТО ЭТА СТРОКА РАСКОММЕНТИРОВАНА И КОРРЕКТНА cors: { origin: socketCorsOrigin, methods: ["GET", "POST"], credentials: true }, // transports: ['websocket', 'polling'], // Можно оставить по умолчанию или указать явно }); console.log(`[BC Server] Socket.IO server configured with path: ${io.path()} and CORS origin: ${socketCorsOrigin === '*' ? "'*'" : socketCorsOrigin || 'Not set'}`); const gameManager = new GameManager(io); const loggedInUsers = {}; // --- MIDDLEWARE АУТЕНТИФИКАЦИИ SOCKET.IO --- io.use(async (socket, next) => { const token = socket.handshake.auth.token; const clientIp = socket.handshake.address; // Может быть IP прокси, если xfwd не настроен на уровне Socket.IO console.log(`[BC Socket.IO Middleware] Auth attempt for socket ${socket.id} from IP ${clientIp}. Token ${token ? 'present' : 'absent'}. Origin: ${socket.handshake.headers.origin}. Path: ${socket.nsp.name}`); 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}`); } } else { console.log(`[BC Socket.IO Middleware] Socket ${socket.id} has no token. Proceeding as unauthenticated.`); } next(); }); // --- ОБРАБОТЧИКИ СОБЫТИЙ SOCKET.IO --- io.on('connection', (socket) => { if (socket.userData && socket.userData.userId) { console.log(`[BC Socket.IO] Authenticated user ${socket.userData.username} (ID: ${socket.userData.userId}) connected with socket: ${socket.id} to path ${socket.nsp.name}`); loggedInUsers[socket.id] = socket.userData; if (gameManager && typeof gameManager.handleRequestGameState === 'function') { gameManager.handleRequestGameState(socket, socket.userData.userId); } else { console.error("[BC Socket.IO] CRITICAL: gameManager or handleRequestGameState not available on connect for authenticated user!"); } } else { console.log(`[BC Socket.IO] Unauthenticated user connected with socket: ${socket.id} to path ${socket.nsp.name}. No game state will be restored.`); } // ... (остальные обработчики событий: logout, playerSurrender, createGame, и т.д. остаются как в вашей версии с логами) ... // Копирую их из вашего предыдущего варианта для полноты: socket.on('logout', () => { const username = socket.userData?.username || 'UnknownUser'; const userId = socket.userData?.userId; console.log(`[BC Socket.IO] 'logout' event from user ${username} (ID: ${userId}, Socket: ${socket.id})`); if (loggedInUsers[socket.id]) { delete loggedInUsers[socket.id]; } socket.userData = null; console.log(`[BC Socket.IO] User ${username} (Socket: ${socket.id}) session data cleared on server due to 'logout' event.`); }); 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('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'}).`); const availableGames = gameManager.getAvailablePvPGamesListForClient(); socket.emit('availablePvPGamesList', availableGames); }); 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 || 'UnauthenticatedUser'; console.log(`[BC Socket.IO] User ${username} (ID: ${identifier || 'N/A'}, Socket: ${socket.id}) disconnected. Reason: ${reason}.`); if (identifier) { gameManager.handleDisconnect(socket.id, identifier); } if (loggedInUsers[socket.id]) { delete loggedInUsers[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] Некорректное значение для BC_APP_PORT: "${process.env.BC_APP_PORT}". Ожидается число.`); process.exit(1); } server.listen(PORT, HOSTNAME, () => { console.log(`[BC Server] Battle Club HTTP Application Server running at http://${HOSTNAME}:${PORT}`); if (HOSTNAME === '127.0.0.1') { console.log(`[BC Server] Server is listening on localhost only. This is suitable if a reverse proxy handles external traffic.`); } else if (HOSTNAME === '0.0.0.0') { console.log(`[BC Server] Server is listening on all available network interfaces.`); } else { console.log(`[BC Server] Server is listening on a specific interface: ${HOSTNAME}.`); } console.log(`[BC Server] Static files served from: ${publicPath}`); }); process.on('unhandledRejection', (reason, promise) => { console.error('[BC Server FATAL] Unhandled Rejection at:', promise, 'reason:', reason); }); process.on('uncaughtException', (err) => { console.error('[BC Server FATAL] Uncaught Exception:', err); process.exit(1); });