With DataBaseREdis

This commit is contained in:
2026-01-28 00:49:37 +03:00
parent 4b01c2d4de
commit af62de5569
10 changed files with 1309 additions and 24 deletions

501
server.js
View File

@ -6,13 +6,215 @@ const express = require('express');
const http = require('http');
const { Server } = require('socket.io');
const path = require('path');
const bcrypt = require('bcrypt');
const session = require('express-session');
const redisClient = require('./redis-client');
const app = express();
const server = http.createServer(app);
const io = new Server(server);
// Парсинг JSON для API
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Настройка сессий (будет настроена после инициализации Redis)
// Временная конфигурация сессий, будет обновлена после инициализации Redis
app.use(session({
secret: process.env.SESSION_SECRET || 'star-wars-hearthstone-secret-key-change-in-production',
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
maxAge: 30 * 24 * 60 * 60 * 1000 // 30 дней
}
}));
app.use(express.static(path.join(__dirname, 'public')));
// API для авторизации и регистрации
app.post('/api/register', async (req, res) => {
try {
const { username, password, email } = req.body;
if (!username || !password) {
return res.status(400).json({ error: 'Имя пользователя и пароль обязательны' });
}
if (username.length < 3 || username.length > 20) {
return res.status(400).json({ error: 'Имя пользователя должно быть от 3 до 20 символов' });
}
if (password.length < 6) {
return res.status(400).json({ error: 'Пароль должен быть не менее 6 символов' });
}
// Проверяем, существует ли пользователь
if (redisClient.isRedisAvailable()) {
const existingUser = await redisClient.get(`user:${username}`);
if (existingUser) {
return res.status(400).json({ error: 'Пользователь с таким именем уже существует' });
}
// Хешируем пароль
const hashedPassword = await bcrypt.hash(password, 10);
// Сохраняем пользователя
const userData = {
username,
password: hashedPassword,
email: email || '',
createdAt: Date.now(),
gamesPlayed: 0,
gamesWon: 0,
totalDamage: 0,
totalHealing: 0
};
await redisClient.set(`user:${username}`, JSON.stringify(userData));
if (email) {
await redisClient.set(`user:email:${email}`, username);
}
// Создаем сессию
req.session.userId = username;
req.session.save();
res.json({ success: true, user: { username, email: userData.email } });
} else {
// Без Redis - простая проверка (только для тестов)
req.session.userId = username;
req.session.save();
res.json({ success: true, user: { username, email: email || '' }, warning: 'Работаем без БД' });
}
} catch (error) {
console.error('Ошибка регистрации:', error);
res.status(500).json({ error: 'Ошибка сервера при регистрации' });
}
});
app.post('/api/login', async (req, res) => {
try {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({ error: 'Имя пользователя и пароль обязательны' });
}
if (redisClient.isRedisAvailable()) {
const userDataStr = await redisClient.get(`user:${username}`);
if (!userDataStr) {
return res.status(401).json({ error: 'Неверное имя пользователя или пароль' });
}
const userData = JSON.parse(userDataStr);
const passwordMatch = await bcrypt.compare(password, userData.password);
if (!passwordMatch) {
return res.status(401).json({ error: 'Неверное имя пользователя или пароль' });
}
// Создаем сессию
req.session.userId = username;
req.session.save();
res.json({
success: true,
user: {
username: userData.username,
email: userData.email,
gamesPlayed: userData.gamesPlayed || 0,
gamesWon: userData.gamesWon || 0
}
});
} else {
// Без Redis - простая авторизация (только для тестов)
req.session.userId = username;
req.session.save();
res.json({ success: true, user: { username }, warning: 'Работаем без БД' });
}
} catch (error) {
console.error('Ошибка входа:', error);
res.status(500).json({ error: 'Ошибка сервера при входе' });
}
});
app.post('/api/logout', (req, res) => {
req.session.destroy((err) => {
if (err) {
return res.status(500).json({ error: 'Ошибка при выходе' });
}
res.json({ success: true });
});
});
app.get('/api/user', async (req, res) => {
if (!req.session.userId) {
return res.status(401).json({ error: 'Не авторизован' });
}
if (redisClient.isRedisAvailable()) {
const userDataStr = await redisClient.get(`user:${req.session.userId}`);
if (userDataStr) {
const userData = JSON.parse(userDataStr);
res.json({
user: {
username: userData.username,
email: userData.email,
gamesPlayed: userData.gamesPlayed || 0,
gamesWon: userData.gamesWon || 0,
totalDamage: userData.totalDamage || 0,
totalHealing: userData.totalHealing || 0
}
});
} else {
res.status(404).json({ error: 'Пользователь не найден' });
}
} else {
res.json({ user: { username: req.session.userId }, warning: 'Работаем без БД' });
}
});
app.get('/api/leaderboard', async (req, res) => {
try {
if (!redisClient.isRedisAvailable()) {
return res.json({ leaderboard: [], warning: 'Работаем без БД' });
}
// Получаем всех пользователей
const userKeys = await redisClient.keys('user:*');
const leaderboard = [];
for (const key of userKeys) {
if (key.startsWith('user:email:')) continue; // Пропускаем email индексы
const userDataStr = await redisClient.get(key);
if (userDataStr) {
const userData = JSON.parse(userDataStr);
leaderboard.push({
username: userData.username,
gamesPlayed: userData.gamesPlayed || 0,
gamesWon: userData.gamesWon || 0,
winRate: userData.gamesPlayed > 0 ? ((userData.gamesWon || 0) / userData.gamesPlayed * 100).toFixed(1) : 0,
totalDamage: userData.totalDamage || 0,
totalHealing: userData.totalHealing || 0
});
}
}
// Сортируем по победам, затем по количеству игр
leaderboard.sort((a, b) => {
if (b.gamesWon !== a.gamesWon) return b.gamesWon - a.gamesWon;
return b.gamesPlayed - a.gamesPlayed;
});
res.json({ leaderboard: leaderboard.slice(0, 100) }); // Топ 100
} catch (error) {
console.error('Ошибка получения leaderboard:', error);
res.status(500).json({ error: 'Ошибка сервера' });
}
});
app.get('/api/cards', (req, res) => {
res.json(cardDb);
});
@ -28,6 +230,7 @@ function getCard(room, cardId) {
}
const rooms = new Map(); // code -> { lobby, gameState, gameStarted, faction, turnTimerInterval, turnTimeLeft, turnTimerWarnFired, spectators: [] }
const socketToUsername = new Map(); // socket.id -> username (для обновления статистики)
const TURN_SECONDS = 90;
const TURN_WARN_AT = 15;
@ -1021,6 +1224,32 @@ function handleTransformOnDeath(room, card, ownerIndex) {
}
}
async function updatePlayerStats(playerName, isWinner, stats = {}) {
if (!redisClient.isRedisAvailable() || !playerName) return;
try {
// playerName может быть именем пользователя или socket.id
// Пытаемся найти по имени пользователя
const userDataStr = await redisClient.get(`user:${playerName}`);
if (userDataStr) {
const userData = JSON.parse(userDataStr);
userData.gamesPlayed = (userData.gamesPlayed || 0) + 1;
if (isWinner) {
userData.gamesWon = (userData.gamesWon || 0) + 1;
}
if (stats.totalDamage) {
userData.totalDamage = (userData.totalDamage || 0) + stats.totalDamage;
}
if (stats.totalHealing) {
userData.totalHealing = (userData.totalHealing || 0) + stats.totalHealing;
}
await redisClient.set(`user:${playerName}`, JSON.stringify(userData));
}
} catch (error) {
console.error('Ошибка обновления статистики для', playerName, ':', error);
}
}
function checkGameOver(room) {
const gameState = room.gameState;
const alive = gameState.players.filter((pp) => pp.health > 0);
@ -1038,6 +1267,27 @@ function checkGameOver(room) {
if (alive.length <= 1) {
gameState.phase = 'ended';
gameState.winner = alive.length === 1 ? alive[0] : null;
// Обновляем статистику игроков
if (gameState.winner) {
gameState.players.forEach((player) => {
const isWinner = player.id === gameState.winner.id;
// Подсчитываем статистику из лога
const totalDamage = gameState.log
.filter(log => {
const playerIdx = gameState.players.indexOf(player);
return (log.fromPlayer === playerIdx || log.fromPlayerIndex === playerIdx) && log.damage;
})
.reduce((sum, log) => sum + (log.damage || 0), 0);
// Обновляем асинхронно, не блокируя основной поток
// Используем имя пользователя из маппинга или имя игрока
const username = socketToUsername.get(player.id) || player.name;
updatePlayerStats(username, isWinner, { totalDamage }).catch(err => {
console.error('Ошибка обновления статистики для', username, err);
});
});
}
}
// Если текущий игрок мёртв, переключаем ход
@ -2037,13 +2287,22 @@ function attack(room, socketId, attackerBoardIndex, targetPlayerIndex, targetBoa
}
io.on('connection', (socket) => {
// Получаем имя пользователя от клиента
socket.on('setUsername', (username) => {
if (username) {
socketToUsername.set(socket.id, username);
}
});
socket.on('createRoom', (data) => {
const name = typeof data === 'string' ? data : (data?.name || 'Host');
// Используем имя из маппинга или переданное имя
const username = socketToUsername.get(socket.id);
const name = username || (typeof data === 'string' ? data : (data?.name || 'Host'));
const aiMode = typeof data === 'object' && data?.aiMode === true;
const code = generateRoomCode();
const room = {
code,
lobby: [{ id: socket.id, name: name || 'Host' }],
lobby: [{ id: socket.id, name: name }],
gameState: null,
gameStarted: false,
faction: null,
@ -2107,10 +2366,14 @@ io.on('connection', (socket) => {
return;
}
// Используем имя из маппинга или переданное имя
const username = socketToUsername.get(socket.id);
const playerName = username || name || `Player ${room.lobby.length + 1}`;
if (room.gameStarted) {
// Игра уже началась - подключаемся как наблюдатель
if (!room.spectators) room.spectators = [];
room.spectators.push({ id: socket.id, name: name || 'Наблюдатель' });
room.spectators.push({ id: socket.id, name: playerName });
socket.join(code);
socket.emit('joinedAsSpectator', { code, gameState: room.gameState });
broadcastGameState(room); // Отправляем текущее состояние игры
@ -2122,7 +2385,7 @@ io.on('connection', (socket) => {
socket.emit('error', 'Комната заполнена');
return;
}
room.lobby.push({ id: socket.id, name: name || `Player ${room.lobby.length + 1}` });
room.lobby.push({ id: socket.id, name: playerName });
socket.join(code);
io.to(code).emit('lobbyUpdate', room.lobby);
socket.emit('roomJoined', { code, you: room.lobby[room.lobby.length - 1], lobby: room.lobby });
@ -2339,6 +2602,8 @@ io.on('connection', (socket) => {
});
socket.on('disconnect', () => {
// Удаляем маппинг socket.id -> username
socketToUsername.delete(socket.id);
const room = getRoomBySocket(socket.id);
if (!room) return;
@ -2382,25 +2647,227 @@ io.on('connection', (socket) => {
let serverIP = 'localhost';
let serverPort = PORT;
server.listen(PORT, '0.0.0.0', () => {
const os = require('os');
const nets = os.networkInterfaces();
let lan = 'localhost';
for (const name of Object.keys(nets)) {
for (const n of nets[name]) {
if (n.family === 'IPv4' && !n.internal) {
lan = n.address;
break;
// API для авторизации и регистрации
app.post('/api/register', async (req, res) => {
try {
const { username, password, email } = req.body;
if (!username || !password) {
return res.status(400).json({ error: 'Имя пользователя и пароль обязательны' });
}
if (username.length < 3 || username.length > 20) {
return res.status(400).json({ error: 'Имя пользователя должно быть от 3 до 20 символов' });
}
if (password.length < 6) {
return res.status(400).json({ error: 'Пароль должен быть не менее 6 символов' });
}
// Проверяем, существует ли пользователь
if (redisClient.isRedisAvailable()) {
const existingUser = await redisClient.get(`user:${username}`);
if (existingUser) {
return res.status(400).json({ error: 'Пользователь с таким именем уже существует' });
}
// Хешируем пароль
const hashedPassword = await bcrypt.hash(password, 10);
// Сохраняем пользователя
const userData = {
username,
password: hashedPassword,
email: email || '',
createdAt: Date.now(),
gamesPlayed: 0,
gamesWon: 0,
totalDamage: 0,
totalHealing: 0
};
await redisClient.set(`user:${username}`, JSON.stringify(userData));
await redisClient.set(`user:email:${email || ''}`, username); // Для поиска по email
// Создаем сессию
req.session.userId = username;
req.session.save();
res.json({ success: true, user: { username, email: userData.email } });
} else {
// Без Redis - простая проверка (только для тестов)
req.session.userId = username;
req.session.save();
res.json({ success: true, user: { username, email: email || '' }, warning: 'Работаем без БД' });
}
} catch (error) {
console.error('Ошибка регистрации:', error);
res.status(500).json({ error: 'Ошибка сервера при регистрации' });
}
});
app.post('/api/login', async (req, res) => {
try {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({ error: 'Имя пользователя и пароль обязательны' });
}
if (redisClient.isRedisAvailable()) {
const userDataStr = await redisClient.get(`user:${username}`);
if (!userDataStr) {
return res.status(401).json({ error: 'Неверное имя пользователя или пароль' });
}
const userData = JSON.parse(userDataStr);
const passwordMatch = await bcrypt.compare(password, userData.password);
if (!passwordMatch) {
return res.status(401).json({ error: 'Неверное имя пользователя или пароль' });
}
// Создаем сессию
req.session.userId = username;
req.session.save();
res.json({
success: true,
user: {
username: userData.username,
email: userData.email,
gamesPlayed: userData.gamesPlayed || 0,
gamesWon: userData.gamesWon || 0
}
});
} else {
// Без Redis - простая авторизация (только для тестов)
req.session.userId = username;
req.session.save();
res.json({ success: true, user: { username }, warning: 'Работаем без БД' });
}
} catch (error) {
console.error('Ошибка входа:', error);
res.status(500).json({ error: 'Ошибка сервера при входе' });
}
});
app.post('/api/logout', (req, res) => {
req.session.destroy((err) => {
if (err) {
return res.status(500).json({ error: 'Ошибка при выходе' });
}
res.json({ success: true });
});
});
app.get('/api/user', async (req, res) => {
if (!req.session.userId) {
return res.status(401).json({ error: 'Не авторизован' });
}
if (redisClient.isRedisAvailable()) {
const userDataStr = await redisClient.get(`user:${req.session.userId}`);
if (userDataStr) {
const userData = JSON.parse(userDataStr);
res.json({
user: {
username: userData.username,
email: userData.email,
gamesPlayed: userData.gamesPlayed || 0,
gamesWon: userData.gamesWon || 0,
totalDamage: userData.totalDamage || 0,
totalHealing: userData.totalHealing || 0
}
});
} else {
res.status(404).json({ error: 'Пользователь не найден' });
}
} else {
res.json({ user: { username: req.session.userId }, warning: 'Работаем без БД' });
}
});
app.get('/api/leaderboard', async (req, res) => {
try {
if (!redisClient.isRedisAvailable()) {
return res.json({ leaderboard: [], warning: 'Работаем без БД' });
}
// Получаем всех пользователей
const userKeys = await redisClient.keys('user:*');
const leaderboard = [];
for (const key of userKeys) {
if (key.startsWith('user:email:')) continue; // Пропускаем email индексы
const userDataStr = await redisClient.get(key);
if (userDataStr) {
const userData = JSON.parse(userDataStr);
leaderboard.push({
username: userData.username,
gamesPlayed: userData.gamesPlayed || 0,
gamesWon: userData.gamesWon || 0,
winRate: userData.gamesPlayed > 0 ? ((userData.gamesWon || 0) / userData.gamesPlayed * 100).toFixed(1) : 0,
totalDamage: userData.totalDamage || 0,
totalHealing: userData.totalHealing || 0
});
}
}
// Сортируем по победам, затем по количеству игр
leaderboard.sort((a, b) => {
if (b.gamesWon !== a.gamesWon) return b.gamesWon - a.gamesWon;
return b.gamesPlayed - a.gamesPlayed;
});
res.json({ leaderboard: leaderboard.slice(0, 100) }); // Топ 100
} catch (error) {
console.error('Ошибка получения leaderboard:', error);
res.status(500).json({ error: 'Ошибка сервера' });
}
serverIP = lan;
serverPort = PORT;
console.log(`
});
// Middleware для проверки авторизации
function requireAuth(req, res, next) {
if (!req.session.userId) {
return res.status(401).json({ error: 'Требуется авторизация' });
}
next();
}
// Инициализация Redis перед запуском сервера
(async () => {
await redisClient.initRedis();
// Настраиваем session store после инициализации Redis
// Сессии уже настроены выше, Redis используется только для данных пользователей
if (redisClient.isRedisAvailable()) {
console.log('💾 Redis доступен для хранения данных пользователей');
} else {
console.log('💾 Работаем без БД (Redis недоступен)');
}
server.listen(PORT, '0.0.0.0', () => {
const os = require('os');
const nets = os.networkInterfaces();
let lan = 'localhost';
for (const name of Object.keys(nets)) {
for (const n of nets[name]) {
if (n.family === 'IPv4' && !n.internal) {
lan = n.address;
break;
}
}
}
serverIP = lan;
serverPort = PORT;
console.log(`
╔══════════════════════════════════════════════════════════╗
║ STAR WARS HEARTHSTONE - Server running ║
║ Local: http://localhost:${PORT}
║ LAN: http://${lan}:${PORT} (use for Radmin VPN) ║
║ Redis: ${redisClient.isRedisAvailable() ? '✅ подключен' : '❌ отключен (работаем без БД)'}
╚══════════════════════════════════════════════════════════╝
`);
});
});
})();