bc/server/bc.js

309 lines
19 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
// Загружаем переменные окружения из .env файла
// Убедитесь, что `dotenv` установлен (npm install dotenv)
// и этот вызов находится как можно раньше
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'); // Предполагается, что db.js корректно инициализирует соединение
const GAME_CONFIG = require('./core/config'); // Глобальная конфигурация игры
const app = express();
const server = http.createServer(app);
// --- НАСТРОЙКА EXPRESS ---
// Определяем разрешенный источник для HTTP CORS
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 // Если вы планируете использовать куки или заголовки авторизации с CORS
}));
app.use(express.json());
// Раздача статических файлов из папки public уровнем выше (../public)
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 CORS
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: '/socket.io/', // Оставьте эту строку ЗАКОММЕНТИРОВАННОЙ, если клиент подключается к /socket.io/ по умолчанию,
// И ваш прокси НЕ отрезает /socket.io/ (т.е. stripPrefix: false для /socket.io в config.json)
// Если клиент подключается к /, а прокси добавляет /socket.io/, то здесь нужно '/'.
// Если клиент подключается к /socket.io/, а прокси ОТРЕЗАЕТ /socket.io/ (stripPrefix: true), то здесь не нужен path.
// Для вашей конфигурации прокси (stripPrefix: false для /socket.io), и если клиент обращается к /socket.io/,
// то здесь ЛИБО path: '/socket.io/', ЛИБО клиент должен обращаться к wss://domain.com/ (без /socket.io/)
// и тогда прокси должен добавлять /socket.io в target.
// САМЫЙ ПРОСТОЙ ВАРИАНТ: если клиент идет на /socket.io/ (стандарт), и прокси НЕ режет /socket.io/ (stripPrefix: false),
// то здесь path должен быть '/socket.io/'.
path: '/socket.io/', // <--- РАСКОММЕНТИРУЙТЕ ЭТО, если клиент подключается к /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()}`);
const gameManager = new GameManager(io); // GameManager должен быть инициализирован ПОСЛЕ io
const loggedInUsers = {}; // { socket.id: { userId, username } }
// --- MIDDLEWARE АУТЕНТИФИКАЦИИ SOCKET.IO ---
io.use(async (socket, next) => {
const token = socket.handshake.auth.token;
const clientIp = socket.handshake.address;
console.log(`[BC Socket.IO Middleware] Auth attempt for socket ${socket.id} from IP ${clientIp}. Token ${token ? 'present' : 'absent'}. Origin: ${socket.handshake.headers.origin}`);
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}`);
// не вызываем next(new Error(...)) чтобы разрешить неаутентифицированные подключения,
// но socket.userData не будет установлен. Логика в 'connection' должна это учитывать.
}
} else {
console.log(`[BC Socket.IO Middleware] Socket ${socket.id} has no token. Proceeding as unauthenticated.`);
}
// Позволяем неаутентифицированным сокетам подключаться,
// но они не смогут выполнять действия, требующие userData
next();
});
// ------------------------------------
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}`);
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}. No game state will be restored.`);
// Можно отправить клиенту сообщение, что он не аутентифицирован, если это необходимо
// socket.emit('authError', { message: 'Вы не аутентифицированы.' });
}
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})`);
// GameManager.handleDisconnect будет вызван автоматически при событии 'disconnect',
// которое произойдет после того, как клиент разорвет соединение или обновит токен.
// Если игрок нажал "выход", но еще в игре, клиент пошлет 'playerSurrender' ДО этого.
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'; // По умолчанию 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 об отключении пользователя, чтобы он мог обработать логику игры
// (например, пометить игрока как временно отключенного, запустить таймер реконнекта)
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'; // '0.0.0.0' для прослушивания на всех интерфейсах
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}`);
console.log(`[BC Server] Socket.IO server using path: ${io.path()} and CORS origin: ${socketCorsOrigin === '*' ? "'*'" : socketCorsOrigin || 'Not set'}`);
console.log(`[BC Server] HTTP API CORS origin: ${clientOrigin === '*' ? "'*'" : clientOrigin || 'Not set'}`);
});
// Глобальные обработчики необработанных ошибок
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);
// Важно: после uncaughtException приложение находится в непредсказуемом состоянии
// и должно быть перезапущено.
process.exit(1);
});