bc/server/bc.js

276 lines
14 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');
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);
});