327 lines
18 KiB
JavaScript
327 lines
18 KiB
JavaScript
// /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);
|
||
}); |