bc/server/bc.js
2025-05-25 14:16:48 +03:00

287 lines
15 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 ---
console.log(`[BC.JS CONFIG] Reading environment variables...`);
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);
// Этот лог покажет, какое значение clientOrigin будет использовано для HTTP CORS
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) { // Добавил проверку на undefined NODE_ENV
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 // Важно, если клиент шлет куки или заголовок Authorization
}));
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;
// Логируем входящий Origin для этого запроса
console.log(`[BC HTTP /auth/register] Attempt for username: "${username}" from IP: ${req.ip}. Origin header: ${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;
// Логируем входящий Origin для этого запроса
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 ---
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);
// Этот лог покажет, какое значение socketCorsOrigin будет использовано для Socket.IO CORS
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) { // Добавил проверку на undefined NODE_ENV
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 loggedInUsers = {};
// --- MIDDLEWARE АУТЕНТИФИКАЦИИ SOCKET.IO ---
io.use(async (socket, next) => {
const token = socket.handshake.auth.token;
// Пытаемся получить IP клиента, учитывая возможные заголовки от прокси
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}`);
}
} 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}`);
loggedInUsers[socket.id] = socket.userData;
if (gameManager && typeof gameManager.handleRequestGameState === 'function') {
gameManager.handleRequestGameState(socket, socket.userData.userId);
} else {
console.error("[BC Socket.IO Connection] CRITICAL: gameManager or handleRequestGameState not available for authenticated user!");
}
} else {
console.log(`[BC Socket.IO Connection] Unauthenticated user connected. Socket: ${socket.id}, IP: ${clientIp}, Origin: ${originHeader}, Path: ${socketPath}. 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 'logout'] User ${username} (Socket: ${socket.id}) session data cleared.`);
});
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 Disconnect] 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] 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: ${app.get('env') === 'development' && !process.env.CORS_ORIGIN_CLIENT ? "'*'" : clientOrigin || 'NOT SET'}`); // Это немного упрощенный вывод, точнее в логах CONFIG
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);
});