feat(auth): implement client-side authentication module for Texas Hold'em game

- Added functions for user login, registration, and guest play
- Implemented token management and user session handling
- Created UI updates for user display and admin panel settings
- Integrated server communication for user settings and logs
- Added error handling and notifications for user actions
This commit is contained in:
ur002 2026-02-01 13:14:21 +03:00
parent 1e957db264
commit 9138a58e72
9 changed files with 1916 additions and 57 deletions

567
database.js Normal file
View File

@ -0,0 +1,567 @@
/**
* =============================================================================
* Texas Hold'em - Модуль базы данных (SQLite)
* =============================================================================
*/
const initSqlJs = require('sql.js');
const fs = require('fs');
const path = require('path');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const { v4: uuidv4 } = require('uuid');
// Путь к файлу БД
const DB_PATH = path.join(__dirname, 'poker.db');
const JWT_SECRET = process.env.JWT_SECRET || 'poker-secret-key-change-in-production';
const JWT_EXPIRES = '7d';
let db = null;
/**
* Инициализация базы данных
*/
async function initDatabase() {
const SQL = await initSqlJs();
// Загружаем существующую БД или создаём новую
if (fs.existsSync(DB_PATH)) {
const buffer = fs.readFileSync(DB_PATH);
db = new SQL.Database(buffer);
console.log('📦 База данных загружена');
} else {
db = new SQL.Database();
console.log('📦 Создана новая база данных');
}
// Создаём таблицы
createTables();
// Создаём админа по умолчанию
await createDefaultAdmin();
return db;
}
/**
* Создание таблиц
*/
function createTables() {
// Пользователи
db.run(`
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
username TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
role TEXT DEFAULT 'user',
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
last_login TEXT,
total_games INTEGER DEFAULT 0,
total_wins INTEGER DEFAULT 0,
total_chips_won INTEGER DEFAULT 0
)
`);
// Настройки пользователей
db.run(`
CREATE TABLE IF NOT EXISTS user_settings (
user_id TEXT PRIMARY KEY,
sound INTEGER DEFAULT 1,
animations INTEGER DEFAULT 1,
show_hand_strength INTEGER DEFAULT 1,
autofold INTEGER DEFAULT 1,
card_back_style TEXT DEFAULT 'default',
card_back_custom TEXT,
FOREIGN KEY (user_id) REFERENCES users(id)
)
`);
// Админские настройки (глобальные)
db.run(`
CREATE TABLE IF NOT EXISTS admin_settings (
key TEXT PRIMARY KEY,
value TEXT,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_by TEXT
)
`);
// Логи действий
db.run(`
CREATE TABLE IF NOT EXISTS action_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT DEFAULT CURRENT_TIMESTAMP,
user_id TEXT,
username TEXT,
action_type TEXT NOT NULL,
action_data TEXT,
room_id TEXT,
ip_address TEXT
)
`);
// Чат сообщения
db.run(`
CREATE TABLE IF NOT EXISTS chat_messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT DEFAULT CURRENT_TIMESTAMP,
room_id TEXT,
room_type TEXT,
user_id TEXT,
username TEXT NOT NULL,
message TEXT NOT NULL,
is_system INTEGER DEFAULT 0
)
`);
// Сессии игр
db.run(`
CREATE TABLE IF NOT EXISTS game_sessions (
id TEXT PRIMARY KEY,
room_id TEXT,
started_at TEXT DEFAULT CURRENT_TIMESTAMP,
ended_at TEXT,
players TEXT,
winner_id TEXT,
pot_size INTEGER,
game_data TEXT
)
`);
saveDatabase();
console.log('📋 Таблицы созданы');
}
/**
* Создать админа по умолчанию
*/
async function createDefaultAdmin() {
const adminExists = db.exec("SELECT id FROM users WHERE role = 'admin' LIMIT 1");
if (adminExists.length === 0 || adminExists[0].values.length === 0) {
const adminId = uuidv4();
const passwordHash = bcrypt.hashSync('admin123', 10);
db.run(`
INSERT INTO users (id, username, password_hash, role)
VALUES (?, ?, ?, 'admin')
`, [adminId, 'admin', passwordHash]);
saveDatabase();
console.log('👑 Создан администратор по умолчанию: admin / admin123');
}
}
/**
* Сохранить БД на диск
*/
function saveDatabase() {
if (db) {
const data = db.export();
const buffer = Buffer.from(data);
fs.writeFileSync(DB_PATH, buffer);
}
}
// =============================================================================
// ПОЛЬЗОВАТЕЛИ И АВТОРИЗАЦИЯ
// =============================================================================
/**
* Регистрация пользователя
*/
function registerUser(username, password) {
try {
// Проверяем, существует ли пользователь
const existing = db.exec("SELECT id FROM users WHERE username = ?", [username]);
if (existing.length > 0 && existing[0].values.length > 0) {
return { success: false, error: 'Пользователь уже существует' };
}
const userId = uuidv4();
const passwordHash = bcrypt.hashSync(password, 10);
db.run(`
INSERT INTO users (id, username, password_hash)
VALUES (?, ?, ?)
`, [userId, username, passwordHash]);
// Создаём настройки по умолчанию
db.run(`
INSERT INTO user_settings (user_id)
VALUES (?)
`, [userId]);
saveDatabase();
// Логируем
logAction(userId, username, 'register', { username });
// Генерируем токен
const token = generateToken(userId, username, 'user');
return {
success: true,
user: { id: userId, username, role: 'user' },
token
};
} catch (error) {
console.error('Ошибка регистрации:', error);
return { success: false, error: 'Ошибка регистрации' };
}
}
/**
* Авторизация пользователя
*/
function loginUser(username, password) {
try {
const result = db.exec(`
SELECT id, username, password_hash, role
FROM users
WHERE username = ?
`, [username]);
if (result.length === 0 || result[0].values.length === 0) {
return { success: false, error: 'Неверный логин или пароль' };
}
const [userId, storedUsername, passwordHash, role] = result[0].values[0];
if (!bcrypt.compareSync(password, passwordHash)) {
return { success: false, error: 'Неверный логин или пароль' };
}
// Обновляем last_login
db.run(`
UPDATE users SET last_login = datetime('now') WHERE id = ?
`, [userId]);
saveDatabase();
// Логируем
logAction(userId, storedUsername, 'login', { username: storedUsername });
// Генерируем токен
const token = generateToken(userId, storedUsername, role);
return {
success: true,
user: { id: userId, username: storedUsername, role },
token
};
} catch (error) {
console.error('Ошибка авторизации:', error);
return { success: false, error: 'Ошибка авторизации' };
}
}
/**
* Генерировать JWT токен
*/
function generateToken(userId, username, role) {
return jwt.sign(
{ userId, username, role },
JWT_SECRET,
{ expiresIn: JWT_EXPIRES }
);
}
/**
* Проверить JWT токен
*/
function verifyToken(token) {
try {
return jwt.verify(token, JWT_SECRET);
} catch (error) {
return null;
}
}
/**
* Получить пользователя по ID
*/
function getUserById(userId) {
const result = db.exec(`
SELECT id, username, role, created_at, last_login, total_games, total_wins, total_chips_won
FROM users
WHERE id = ?
`, [userId]);
if (result.length === 0 || result[0].values.length === 0) {
return null;
}
const [id, username, role, created_at, last_login, total_games, total_wins, total_chips_won] = result[0].values[0];
return { id, username, role, created_at, last_login, total_games, total_wins, total_chips_won };
}
// =============================================================================
// НАСТРОЙКИ
// =============================================================================
/**
* Получить настройки пользователя
*/
function getUserSettings(userId) {
const result = db.exec(`
SELECT sound, animations, show_hand_strength, autofold, card_back_style, card_back_custom
FROM user_settings
WHERE user_id = ?
`, [userId]);
if (result.length === 0 || result[0].values.length === 0) {
return {
sound: true,
animations: true,
showHandStrength: true,
autofold: true,
cardBackStyle: 'default',
cardBackCustom: null
};
}
const [sound, animations, showHandStrength, autofold, cardBackStyle, cardBackCustom] = result[0].values[0];
return {
sound: !!sound,
animations: !!animations,
showHandStrength: !!showHandStrength,
autofold: !!autofold,
cardBackStyle: cardBackStyle || 'default',
cardBackCustom
};
}
/**
* Сохранить настройки пользователя
*/
function saveUserSettings(userId, settings) {
db.run(`
INSERT OR REPLACE INTO user_settings
(user_id, sound, animations, show_hand_strength, autofold, card_back_style, card_back_custom)
VALUES (?, ?, ?, ?, ?, ?, ?)
`, [
userId,
settings.sound ? 1 : 0,
settings.animations ? 1 : 0,
settings.showHandStrength ? 1 : 0,
settings.autofold ? 1 : 0,
settings.cardBackStyle || 'default',
settings.cardBackCustom || null
]);
saveDatabase();
return true;
}
/**
* Получить админские настройки
*/
function getAdminSettings() {
const result = db.exec(`SELECT key, value FROM admin_settings`);
const settings = {
llmEnabled: false,
llmProvider: 'ollama',
llmApiUrl: 'http://localhost:11434',
llmModel: 'llama3.2',
llmApiKey: '',
serverUrl: 'ws://localhost:3000'
};
if (result.length > 0) {
result[0].values.forEach(([key, value]) => {
if (key === 'llmEnabled') {
settings.llmEnabled = value === 'true';
} else {
settings[key] = value;
}
});
}
return settings;
}
/**
* Сохранить админские настройки
*/
function saveAdminSettings(settings, adminId) {
const keys = ['llmEnabled', 'llmProvider', 'llmApiUrl', 'llmModel', 'llmApiKey', 'serverUrl'];
keys.forEach(key => {
if (settings[key] !== undefined) {
const value = typeof settings[key] === 'boolean' ? settings[key].toString() : settings[key];
db.run(`
INSERT OR REPLACE INTO admin_settings (key, value, updated_at, updated_by)
VALUES (?, ?, datetime('now'), ?)
`, [key, value, adminId]);
}
});
saveDatabase();
return true;
}
// =============================================================================
// ЛОГИРОВАНИЕ
// =============================================================================
/**
* Логировать действие
*/
function logAction(userId, username, actionType, actionData = {}, roomId = null, ipAddress = null) {
db.run(`
INSERT INTO action_logs (user_id, username, action_type, action_data, room_id, ip_address)
VALUES (?, ?, ?, ?, ?, ?)
`, [userId, username, actionType, JSON.stringify(actionData), roomId, ipAddress]);
saveDatabase();
}
/**
* Получить логи (для админа)
*/
function getLogs(options = {}) {
const { limit = 100, offset = 0, actionType = null, userId = null, startDate = null, endDate = null } = options;
let sql = `SELECT * FROM action_logs WHERE 1=1`;
const params = [];
if (actionType) {
sql += ` AND action_type = ?`;
params.push(actionType);
}
if (userId) {
sql += ` AND user_id = ?`;
params.push(userId);
}
if (startDate) {
sql += ` AND timestamp >= ?`;
params.push(startDate);
}
if (endDate) {
sql += ` AND timestamp <= ?`;
params.push(endDate);
}
sql += ` ORDER BY timestamp DESC LIMIT ? OFFSET ?`;
params.push(limit, offset);
const result = db.exec(sql, params);
if (result.length === 0) return [];
return result[0].values.map(row => ({
id: row[0],
timestamp: row[1],
userId: row[2],
username: row[3],
actionType: row[4],
actionData: JSON.parse(row[5] || '{}'),
roomId: row[6],
ipAddress: row[7]
}));
}
/**
* Получить статистику логов
*/
function getLogStats() {
const totalResult = db.exec(`SELECT COUNT(*) FROM action_logs`);
const total = totalResult[0]?.values[0]?.[0] || 0;
const byTypeResult = db.exec(`
SELECT action_type, COUNT(*) as count
FROM action_logs
GROUP BY action_type
ORDER BY count DESC
`);
const byType = byTypeResult.length > 0
? byTypeResult[0].values.map(([type, count]) => ({ type, count }))
: [];
const todayResult = db.exec(`
SELECT COUNT(*) FROM action_logs
WHERE date(timestamp) = date('now')
`);
const today = todayResult[0]?.values[0]?.[0] || 0;
return { total, byType, today };
}
// =============================================================================
// ЧАТ
// =============================================================================
/**
* Сохранить сообщение чата
*/
function saveChatMessage(roomId, roomType, userId, username, message, isSystem = false) {
db.run(`
INSERT INTO chat_messages (room_id, room_type, user_id, username, message, is_system)
VALUES (?, ?, ?, ?, ?, ?)
`, [roomId, roomType, userId, username, message, isSystem ? 1 : 0]);
saveDatabase();
}
/**
* Получить историю чата комнаты
*/
function getChatHistory(roomId, limit = 50) {
const result = db.exec(`
SELECT timestamp, username, message, is_system
FROM chat_messages
WHERE room_id = ?
ORDER BY timestamp DESC
LIMIT ?
`, [roomId, limit]);
if (result.length === 0) return [];
return result[0].values.map(row => ({
timestamp: row[0],
username: row[1],
message: row[2],
isSystem: !!row[3]
})).reverse();
}
/**
* Очистить чат комнаты
*/
function clearRoomChat(roomId) {
db.run(`DELETE FROM chat_messages WHERE room_id = ?`, [roomId]);
saveDatabase();
}
// =============================================================================
// ЭКСПОРТ
// =============================================================================
module.exports = {
initDatabase,
saveDatabase,
// Пользователи
registerUser,
loginUser,
verifyToken,
getUserById,
// Настройки
getUserSettings,
saveUserSettings,
getAdminSettings,
saveAdminSettings,
// Логирование
logAction,
getLogs,
getLogStats,
// Чат
saveChatMessage,
getChatHistory,
clearRoomChat
};

150
package-lock.json generated
View File

@ -9,7 +9,11 @@
"version": "1.0.0",
"license": "MIT",
"dependencies": {
"bcryptjs": "^3.0.3",
"express": "^4.18.2",
"jsonwebtoken": "^9.0.3",
"sql.js": "^1.13.0",
"uuid": "^13.0.0",
"ws": "^8.16.0"
}
},
@ -32,6 +36,15 @@
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
"license": "MIT"
},
"node_modules/bcryptjs": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz",
"integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==",
"license": "BSD-3-Clause",
"bin": {
"bcrypt": "bin/bcrypt"
}
},
"node_modules/body-parser": {
"version": "1.20.4",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
@ -56,6 +69,12 @@
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
"license": "BSD-3-Clause"
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@ -172,6 +191,15 @@
"node": ">= 0.4"
}
},
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
"license": "Apache-2.0",
"dependencies": {
"safe-buffer": "^5.0.1"
}
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@ -443,6 +471,97 @@
"node": ">= 0.10"
}
},
"node_modules/jsonwebtoken": {
"version": "9.0.3",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
"integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
"license": "MIT",
"dependencies": {
"jws": "^4.0.1",
"lodash.includes": "^4.3.0",
"lodash.isboolean": "^3.0.3",
"lodash.isinteger": "^4.0.4",
"lodash.isnumber": "^3.0.3",
"lodash.isplainobject": "^4.0.6",
"lodash.isstring": "^4.0.1",
"lodash.once": "^4.0.0",
"ms": "^2.1.1",
"semver": "^7.5.4"
},
"engines": {
"node": ">=12",
"npm": ">=6"
}
},
"node_modules/jsonwebtoken/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/jwa": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
"integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
"license": "MIT",
"dependencies": {
"buffer-equal-constant-time": "^1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"node_modules/jws": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
"integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
"license": "MIT",
"dependencies": {
"jwa": "^2.0.1",
"safe-buffer": "^5.0.1"
}
},
"node_modules/lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
"license": "MIT"
},
"node_modules/lodash.isboolean": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
"license": "MIT"
},
"node_modules/lodash.isinteger": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
"license": "MIT"
},
"node_modules/lodash.isnumber": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
"license": "MIT"
},
"node_modules/lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
"license": "MIT"
},
"node_modules/lodash.isstring": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
"license": "MIT"
},
"node_modules/lodash.once": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
"license": "MIT"
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@ -644,6 +763,18 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
"node_modules/semver": {
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/send": {
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
@ -767,6 +898,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/sql.js": {
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/sql.js/-/sql.js-1.13.0.tgz",
"integrity": "sha512-RJbVP1HRDlUUXahJ7VMTcu9Rm1Nzw+EBpoPr94vnbD4LwR715F3CcxE2G2k45PewcaZ57pjetYa+LoSJLAASgA==",
"license": "MIT"
},
"node_modules/statuses": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
@ -816,6 +953,19 @@
"node": ">= 0.4.0"
}
},
"node_modules/uuid": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz",
"integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist-node/bin/uuid"
}
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",

View File

@ -8,10 +8,19 @@
"dev": "node server.js"
},
"dependencies": {
"ws": "^8.16.0",
"express": "^4.18.2"
"bcryptjs": "^3.0.3",
"express": "^4.18.2",
"jsonwebtoken": "^9.0.3",
"sql.js": "^1.13.0",
"uuid": "^13.0.0",
"ws": "^8.16.0"
},
"keywords": ["poker", "texas-holdem", "websocket", "game"],
"keywords": [
"poker",
"texas-holdem",
"websocket",
"game"
],
"author": "",
"license": "MIT"
}

BIN
poker.db Normal file

Binary file not shown.

543
public/auth.js Normal file
View File

@ -0,0 +1,543 @@
/**
* =============================================================================
* Texas Hold'em - Модуль авторизации (клиент)
* =============================================================================
*/
// Текущий пользователь
let currentUser = null;
let authToken = null;
let isGuest = false;
// Пагинация логов
let currentLogPage = 1;
const logsPerPage = 20;
// =============================================================================
// АВТОРИЗАЦИЯ
// =============================================================================
/**
* Инициализация авторизации
*/
async function initAuth() {
// Проверяем сохранённый токен
const savedToken = localStorage.getItem('authToken');
if (savedToken) {
try {
const response = await fetch('/api/auth/me', {
headers: { 'Authorization': `Bearer ${savedToken}` }
});
if (response.ok) {
const data = await response.json();
currentUser = data.user;
authToken = savedToken;
onAuthSuccess();
return;
}
} catch (error) {
console.error('Ошибка проверки токена:', error);
}
// Токен недействителен
localStorage.removeItem('authToken');
}
// Показываем экран авторизации
showScreen('auth-screen');
}
/**
* Переключить вкладку авторизации
*/
function switchAuthTab(tab) {
const tabs = document.querySelectorAll('#auth-tabs .btn-tab');
tabs.forEach(t => t.classList.remove('active'));
event.target.classList.add('active');
document.getElementById('login-form').style.display = tab === 'login' ? 'block' : 'none';
document.getElementById('register-form').style.display = tab === 'register' ? 'block' : 'none';
}
/**
* Обработчик входа
*/
async function handleLogin() {
const username = document.getElementById('login-username').value.trim();
const password = document.getElementById('login-password').value;
if (!username || !password) {
showNotification('Введите логин и пароль', 'error');
return;
}
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const data = await response.json();
if (response.ok) {
currentUser = data.user;
authToken = data.token;
localStorage.setItem('authToken', authToken);
isGuest = false;
onAuthSuccess();
} else {
showNotification(data.error || 'Ошибка входа', 'error');
}
} catch (error) {
console.error('Ошибка входа:', error);
showNotification('Ошибка соединения с сервером', 'error');
}
}
/**
* Обработчик регистрации
*/
async function handleRegister() {
const username = document.getElementById('register-username').value.trim();
const password = document.getElementById('register-password').value;
const passwordConfirm = document.getElementById('register-password-confirm').value;
if (!username || !password) {
showNotification('Заполните все поля', 'error');
return;
}
if (password !== passwordConfirm) {
showNotification('Пароли не совпадают', 'error');
return;
}
if (username.length < 3) {
showNotification('Логин должен быть минимум 3 символа', 'error');
return;
}
if (password.length < 6) {
showNotification('Пароль должен быть минимум 6 символов', 'error');
return;
}
try {
const response = await fetch('/api/auth/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const data = await response.json();
if (response.ok) {
currentUser = data.user;
authToken = data.token;
localStorage.setItem('authToken', authToken);
isGuest = false;
showNotification('Регистрация успешна!', 'success');
onAuthSuccess();
} else {
showNotification(data.error || 'Ошибка регистрации', 'error');
}
} catch (error) {
console.error('Ошибка регистрации:', error);
showNotification('Ошибка соединения с сервером', 'error');
}
}
/**
* Играть как гость
*/
function playAsGuest() {
currentUser = {
id: 'guest_' + Date.now(),
username: ость_' + Math.floor(Math.random() * 10000),
role: 'guest'
};
isGuest = true;
authToken = null;
onAuthSuccess();
}
/**
* Выход
*/
function handleLogout() {
currentUser = null;
authToken = null;
isGuest = false;
localStorage.removeItem('authToken');
showScreen('auth-screen');
showNotification('Вы вышли из системы', 'info');
}
/**
* После успешной авторизации
*/
function onAuthSuccess() {
showScreen('main-menu');
updateUserDisplay();
loadLLMStatus();
// Загружаем настройки с сервера если не гость
if (!isGuest && authToken) {
loadUserSettingsFromServer();
}
}
/**
* Обновить отображение пользователя
*/
function updateUserDisplay() {
const nameEl = document.getElementById('user-display-name');
const badgeEl = document.getElementById('user-role-badge');
const adminBtn = document.getElementById('admin-btn');
if (currentUser) {
nameEl.textContent = currentUser.username;
if (currentUser.role === 'admin') {
badgeEl.style.display = 'inline';
badgeEl.textContent = 'ADMIN';
if (adminBtn) adminBtn.style.display = 'block';
} else {
badgeEl.style.display = 'none';
if (adminBtn) adminBtn.style.display = 'none';
}
// Обновляем поля имени в формах
document.getElementById('sp-player-name').value = currentUser.username;
document.getElementById('mp-player-name').value = currentUser.username;
}
}
// =============================================================================
// НАСТРОЙКИ С СЕРВЕРА
// =============================================================================
/**
* Загрузить пользовательские настройки с сервера
*/
async function loadUserSettingsFromServer() {
if (!authToken) return;
try {
const response = await fetch('/api/settings/user', {
headers: { 'Authorization': `Bearer ${authToken}` }
});
if (response.ok) {
const data = await response.json();
applyServerSettings(data.settings);
}
} catch (error) {
console.error('Ошибка загрузки настроек:', error);
}
}
/**
* Сохранить настройки на сервер
*/
async function saveUserSettingsToServer(settings) {
if (!authToken) return;
try {
await fetch('/api/settings/user', {
method: 'POST',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(settings)
});
} catch (error) {
console.error('Ошибка сохранения настроек:', error);
}
}
/**
* Применить настройки с сервера
*/
function applyServerSettings(serverSettings) {
settings = {
...settings,
sound: serverSettings.sound,
animations: serverSettings.animations,
showHandStrength: serverSettings.showHandStrength,
autofold: serverSettings.autofold,
cardBackStyle: serverSettings.cardBackStyle,
cardBackCustom: serverSettings.cardBackCustom
};
// Применяем к UI
document.getElementById('setting-sound').checked = settings.sound !== false;
document.getElementById('setting-animations').checked = settings.animations !== false;
document.getElementById('setting-hand-strength').checked = settings.showHandStrength !== false;
document.getElementById('setting-autofold').checked = settings.autofold !== false;
soundEnabled = settings.sound !== false;
// Применяем рубашку карт
if (serverSettings.cardBackCustom) {
localStorage.setItem('customCardBack', serverSettings.cardBackCustom);
}
applyCardBackStyle(serverSettings.cardBackStyle || 'default');
}
/**
* Загрузить статус LLM
*/
async function loadLLMStatus() {
try {
const response = await fetch('/api/settings/public');
const data = await response.json();
const statusBox = document.getElementById('llm-status-box');
const statusIcon = document.getElementById('llm-status-icon');
const statusText = document.getElementById('llm-status-text');
if (statusBox) {
if (data.llmEnabled) {
statusBox.classList.add('enabled');
statusBox.classList.remove('disabled');
statusIcon.textContent = '✅';
statusText.textContent = `LLM чат включён (${data.llmProvider})`;
// Сохраняем настройки LLM глобально
window.llmSettings = {
enabled: true,
provider: data.llmProvider,
apiUrl: data.llmApiUrl,
model: data.llmModel
};
} else {
statusBox.classList.add('disabled');
statusBox.classList.remove('enabled');
statusIcon.textContent = '❌';
statusText.textContent = 'LLM чат отключён администратором';
window.llmSettings = { enabled: false };
}
}
} catch (error) {
console.error('Ошибка загрузки статуса LLM:', error);
}
}
// =============================================================================
// АДМИН-ПАНЕЛЬ
// =============================================================================
/**
* Переключить вкладку админки
*/
function switchAdminTab(tab) {
const tabs = document.querySelectorAll('.admin-tabs .btn-tab');
tabs.forEach(t => t.classList.remove('active'));
event.target.classList.add('active');
document.querySelectorAll('.admin-tab-content').forEach(el => {
el.style.display = 'none';
});
document.getElementById(`admin-tab-${tab}`).style.display = 'block';
// Загружаем данные вкладки
if (tab === 'logs') {
loadLogStats();
loadLogs(1);
} else if (tab === 'settings') {
loadAdminSettings();
}
}
/**
* Загрузить админские настройки
*/
async function loadAdminSettings() {
if (!authToken || currentUser?.role !== 'admin') return;
try {
const response = await fetch('/api/settings/admin', {
headers: { 'Authorization': `Bearer ${authToken}` }
});
if (response.ok) {
const data = await response.json();
const s = data.settings;
document.getElementById('admin-llm-enabled').checked = s.llmEnabled;
document.getElementById('admin-llm-provider').value = s.llmProvider || 'ollama';
document.getElementById('admin-llm-url').value = s.llmApiUrl || 'http://localhost:11434';
document.getElementById('admin-llm-model').value = s.llmModel || 'llama3.2';
document.getElementById('admin-llm-apikey').value = s.llmApiKey || '';
}
} catch (error) {
console.error('Ошибка загрузки админ настроек:', error);
}
}
/**
* Сохранить админские настройки
*/
async function saveAdminSettings() {
if (!authToken || currentUser?.role !== 'admin') return;
const settings = {
llmEnabled: document.getElementById('admin-llm-enabled').checked,
llmProvider: document.getElementById('admin-llm-provider').value,
llmApiUrl: document.getElementById('admin-llm-url').value,
llmModel: document.getElementById('admin-llm-model').value,
llmApiKey: document.getElementById('admin-llm-apikey').value
};
try {
const response = await fetch('/api/settings/admin', {
method: 'POST',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(settings)
});
if (response.ok) {
showNotification('Настройки сохранены', 'success');
}
} catch (error) {
showNotification('Ошибка сохранения', 'error');
}
}
/**
* Тестировать LLM из админки
*/
async function testAdminLLM() {
// Сохраняем настройки сначала
await saveAdminSettings();
showNotification('Тестирование LLM...', 'info');
// Здесь можно добавить реальный тест через сервер
setTimeout(() => {
showNotification('LLM тест выполнен (проверьте логи сервера)', 'success');
}, 1000);
}
/**
* Загрузить статистику логов
*/
async function loadLogStats() {
if (!authToken || currentUser?.role !== 'admin') return;
try {
const response = await fetch('/api/logs/stats', {
headers: { 'Authorization': `Bearer ${authToken}` }
});
if (response.ok) {
const data = await response.json();
document.getElementById('stat-total').textContent = data.stats.total;
document.getElementById('stat-today').textContent = data.stats.today;
}
} catch (error) {
console.error('Ошибка загрузки статистики:', error);
}
}
/**
* Загрузить логи
*/
async function loadLogs(page = 1) {
if (!authToken || currentUser?.role !== 'admin') return;
currentLogPage = Math.max(1, page);
const offset = (currentLogPage - 1) * logsPerPage;
const actionType = document.getElementById('log-filter-type').value;
try {
let url = `/api/logs?limit=${logsPerPage}&offset=${offset}`;
if (actionType) url += `&actionType=${actionType}`;
const response = await fetch(url, {
headers: { 'Authorization': `Bearer ${authToken}` }
});
if (response.ok) {
const data = await response.json();
renderLogs(data.logs);
// Обновляем пагинацию
document.getElementById('log-page-info').textContent = `Страница ${currentLogPage}`;
document.getElementById('log-prev-btn').disabled = currentLogPage === 1;
document.getElementById('log-next-btn').disabled = data.logs.length < logsPerPage;
}
} catch (error) {
console.error('Ошибка загрузки логов:', error);
}
}
/**
* Отрисовать логи
*/
function renderLogs(logs) {
const tbody = document.getElementById('log-table-body');
tbody.innerHTML = '';
if (logs.length === 0) {
tbody.innerHTML = '<tr><td colspan="4" style="text-align: center; color: var(--text-muted);">Логи не найдены</td></tr>';
return;
}
logs.forEach(log => {
const tr = document.createElement('tr');
const time = new Date(log.timestamp).toLocaleString('ru-RU');
const details = typeof log.actionData === 'object'
? JSON.stringify(log.actionData).substring(0, 50) + '...'
: log.actionData;
tr.innerHTML = `
<td>${time}</td>
<td>${log.username || 'Гость'}</td>
<td><span class="log-action-badge ${log.actionType}">${log.actionType}</span></td>
<td title="${JSON.stringify(log.actionData)}">${details}</td>
`;
tbody.appendChild(tr);
});
}
// =============================================================================
// ОЧИСТКА ЧАТА
// =============================================================================
/**
* Очистить чат при смене режима игры
*/
function clearGameChat() {
const container = document.getElementById('game-chat-messages');
if (container) {
container.innerHTML = '';
}
}
/**
* Очистить чат лобби
*/
function clearLobbyChat() {
const container = document.getElementById('lobby-chat-messages');
if (container) {
container.innerHTML = '';
}
}
// Экспортируем для глобального использования
window.currentUser = currentUser;
window.authToken = authToken;
window.isGuest = isGuest;

View File

@ -8,8 +8,8 @@
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
</head>
<body>
<!-- Главное меню -->
<div id="main-menu" class="screen active">
<!-- Экран авторизации -->
<div id="auth-screen" class="screen active">
<div class="glass-container menu-container">
<div class="logo">
<span class="logo-icon">🃏</span>
@ -17,6 +17,64 @@
<p class="subtitle">No-Limit Poker</p>
</div>
<div id="auth-tabs" class="auth-tabs">
<button class="btn btn-tab active" onclick="switchAuthTab('login')">Вход</button>
<button class="btn btn-tab" onclick="switchAuthTab('register')">Регистрация</button>
</div>
<!-- Форма входа -->
<div id="login-form" class="auth-form">
<div class="form-group">
<label>Логин</label>
<input type="text" id="login-username" class="input" placeholder="Введите логин" autocomplete="username">
</div>
<div class="form-group">
<label>Пароль</label>
<input type="password" id="login-password" class="input" placeholder="Введите пароль" autocomplete="current-password">
</div>
<button class="btn btn-primary btn-large" onclick="handleLogin()">Войти</button>
</div>
<!-- Форма регистрации -->
<div id="register-form" class="auth-form" style="display: none;">
<div class="form-group">
<label>Логин</label>
<input type="text" id="register-username" class="input" placeholder="Минимум 3 символа" autocomplete="username">
</div>
<div class="form-group">
<label>Пароль</label>
<input type="password" id="register-password" class="input" placeholder="Минимум 6 символов" autocomplete="new-password">
</div>
<div class="form-group">
<label>Повторите пароль</label>
<input type="password" id="register-password-confirm" class="input" placeholder="Повторите пароль" autocomplete="new-password">
</div>
<button class="btn btn-primary btn-large" onclick="handleRegister()">Зарегистрироваться</button>
</div>
<div class="auth-guest">
<span>или</span>
<button class="btn btn-outline" onclick="playAsGuest()">Играть как гость</button>
</div>
</div>
</div>
<!-- Главное меню -->
<div id="main-menu" class="screen">
<div class="glass-container menu-container">
<div class="logo">
<span class="logo-icon">🃏</span>
<h1>Texas Hold'em</h1>
<p class="subtitle">No-Limit Poker</p>
</div>
<!-- Информация о пользователе -->
<div id="user-info" class="user-info">
<span id="user-display-name">Гость</span>
<span id="user-role-badge" class="role-badge" style="display: none;">ADMIN</span>
<button class="btn btn-small btn-outline" onclick="handleLogout()">Выход</button>
</div>
<div class="menu-buttons">
<button class="btn btn-primary btn-large" onclick="showScreen('single-player-menu')">
<span class="btn-icon">🤖</span>
@ -39,6 +97,10 @@
<button class="btn btn-icon-only" onclick="showScreen('settings-screen')">
⚙️
</button>
<!-- Кнопка админ-панели (только для админов) -->
<button class="btn btn-icon-only admin-only" onclick="showScreen('admin-screen')" style="display: none;" id="admin-btn">
👑
</button>
</div>
</div>
</div>
@ -386,50 +448,127 @@
<input type="text" id="server-url" class="input" value="ws://localhost:3000" onchange="updateSettings()">
</div>
<h3 style="margin-top: 24px; margin-bottom: 16px;">🤖 Настройки LLM чата</h3>
<div class="settings-list">
<div class="setting-item">
<span>Включить LLM чат</span>
<label class="switch">
<input type="checkbox" id="setting-llm-enabled" onchange="updateSettings()">
<span class="slider"></span>
</label>
</div>
<!-- LLM статус (только чтение для пользователей) -->
<div class="llm-status-box" id="llm-status-box">
<span class="llm-status-icon" id="llm-status-icon">🤖</span>
<span id="llm-status-text">LLM чат: загрузка...</span>
</div>
<div class="form-group">
<label>LLM провайдер</label>
<input type="hidden" id="llm-provider" value="ollama">
<div class="btn-group">
<button class="btn btn-option active" data-value="ollama" onclick="selectLLMProvider(this)">Ollama</button>
<button class="btn btn-option" data-value="lmstudio" onclick="selectLLMProvider(this)">LM Studio</button>
<button class="btn btn-option" data-value="openai" onclick="selectLLMProvider(this)">OpenAI</button>
</div>
</div>
<div class="form-group">
<label id="llm-api-url-label">URL API</label>
<input type="text" id="llm-api-url" class="input" value="http://localhost:11434" onchange="updateSettings()" placeholder="http://localhost:11434">
</div>
<div class="form-group">
<label>Модель</label>
<input type="text" id="llm-model" class="input" value="llama3.2" onchange="updateSettings()" placeholder="llama3.2, mistral, etc.">
</div>
<div class="form-group" id="llm-api-key-group" style="display: none;">
<label>API Key (для OpenAI)</label>
<input type="password" id="llm-api-key" class="input" onchange="updateSettings()" placeholder="sk-...">
</div>
<button class="btn btn-secondary" id="test-llm-btn" onclick="testLLMConnection()">🔗 Проверить подключение</button>
<div style="margin-top: 16px;"></div>
<button class="btn btn-outline" onclick="resetSettings()">Сбросить настройки</button>
</div>
</div>
<!-- Админ-панель -->
<div id="admin-screen" class="screen">
<div class="glass-container admin-container">
<button class="btn-back" onclick="showScreen('main-menu')">← Назад</button>
<h2>👑 Админ-панель</h2>
<div class="admin-tabs">
<button class="btn btn-tab active" onclick="switchAdminTab('settings')">⚙️ Настройки</button>
<button class="btn btn-tab" onclick="switchAdminTab('logs')">📋 Логи</button>
<button class="btn btn-tab" onclick="switchAdminTab('users')">👥 Пользователи</button>
</div>
<!-- Вкладка настроек -->
<div id="admin-tab-settings" class="admin-tab-content">
<h3>🤖 Настройки LLM чата</h3>
<div class="settings-list">
<div class="setting-item">
<span>Включить LLM чат для всех</span>
<label class="switch">
<input type="checkbox" id="admin-llm-enabled" onchange="saveAdminSettings()">
<span class="slider"></span>
</label>
</div>
</div>
<div class="form-group">
<label>LLM провайдер</label>
<select id="admin-llm-provider" class="input" onchange="saveAdminSettings()">
<option value="ollama">Ollama</option>
<option value="lmstudio">LM Studio</option>
<option value="openai">OpenAI</option>
</select>
</div>
<div class="form-group">
<label>URL API</label>
<input type="text" id="admin-llm-url" class="input" value="http://localhost:11434" onchange="saveAdminSettings()">
</div>
<div class="form-group">
<label>Модель</label>
<input type="text" id="admin-llm-model" class="input" value="llama3.2" onchange="saveAdminSettings()">
</div>
<div class="form-group">
<label>API Key (для OpenAI)</label>
<input type="password" id="admin-llm-apikey" class="input" placeholder="sk-..." onchange="saveAdminSettings()">
</div>
<button class="btn btn-secondary" onclick="testAdminLLM()">🔗 Проверить подключение</button>
</div>
<!-- Вкладка логов -->
<div id="admin-tab-logs" class="admin-tab-content" style="display: none;">
<h3>📋 История действий</h3>
<div class="log-stats" id="log-stats">
<div class="stat-item">
<span class="stat-value" id="stat-total">0</span>
<span class="stat-label">Всего</span>
</div>
<div class="stat-item">
<span class="stat-value" id="stat-today">0</span>
<span class="stat-label">Сегодня</span>
</div>
</div>
<div class="log-filters">
<select id="log-filter-type" class="input" onchange="loadLogs()">
<option value="">Все действия</option>
<option value="login">Входы</option>
<option value="register">Регистрации</option>
<option value="room_created">Создание комнат</option>
<option value="chat_message">Сообщения чата</option>
<option value="game_action">Игровые действия</option>
</select>
<button class="btn btn-secondary" onclick="loadLogs()">🔄 Обновить</button>
</div>
<div class="log-table-container">
<table class="log-table" id="log-table">
<thead>
<tr>
<th>Время</th>
<th>Пользователь</th>
<th>Действие</th>
<th>Детали</th>
</tr>
</thead>
<tbody id="log-table-body">
</tbody>
</table>
</div>
<div class="log-pagination">
<button class="btn btn-small" onclick="loadLogs(currentLogPage - 1)" id="log-prev-btn">← Назад</button>
<span id="log-page-info">Страница 1</span>
<button class="btn btn-small" onclick="loadLogs(currentLogPage + 1)" id="log-next-btn">Вперёд →</button>
</div>
</div>
<!-- Вкладка пользователей -->
<div id="admin-tab-users" class="admin-tab-content" style="display: none;">
<h3>👥 Пользователи</h3>
<p style="color: var(--text-muted);">Раздел в разработке...</p>
</div>
</div>
</div>
<!-- Результаты раздачи -->
<div id="hand-result-modal" class="modal">
<div class="modal-content glass-container">
@ -444,6 +583,7 @@
<script src="game.js"></script>
<script src="ai.js"></script>
<script src="auth.js"></script>
<script src="main.js"></script>
</body>
</html>

View File

@ -60,11 +60,12 @@ document.addEventListener('DOMContentLoaded', () => {
});
}
// Восстанавливаем имя игрока
const savedName = localStorage.getItem('playerName');
if (savedName) {
document.getElementById('sp-player-name').value = savedName;
document.getElementById('mp-player-name').value = savedName;
// Инициализируем авторизацию
if (typeof initAuth === 'function') {
initAuth();
} else {
// Если auth.js не загружен, показываем главное меню
showScreen('main-menu');
}
});
@ -1155,12 +1156,18 @@ function addChatMessage(chatType, sender, message, isSystem = false, isTyping =
/**
* Удалить индикатор "печатает..." для конкретного отправителя
*/
function removeTypingIndicator(sender) {
const container = document.getElementById('game-chat-messages');
const typingMsg = container.querySelector(`.typing-indicator[data-sender="${sender}"]`);
if (typingMsg) {
typingMsg.remove();
}
function removeTypingIndicator(sender, chatType = 'game') {
const containerId = chatType === 'game' ? 'game-chat-messages' : 'lobby-chat-messages';
const container = document.getElementById(containerId);
if (!container) return;
// Используем querySelectorAll и проверяем dataset, чтобы избежать проблем с экранированием кавычек
const typingIndicators = container.querySelectorAll('.typing-indicator');
typingIndicators.forEach(indicator => {
if (indicator.dataset.sender === sender) {
indicator.remove();
}
});
}
/**

View File

@ -1635,3 +1635,241 @@ body::before {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
}
/* =============================================================================
АВТОРИЗАЦИЯ
============================================================================= */
.auth-tabs {
display: flex;
gap: 8px;
margin-bottom: 24px;
}
.btn-tab {
flex: 1;
background: transparent;
border: 1px solid var(--glass-border);
color: var(--text-secondary);
padding: 12px;
cursor: pointer;
transition: all var(--transition-fast);
}
.btn-tab.active {
background: var(--accent-gradient);
border-color: transparent;
color: white;
}
.auth-form {
display: flex;
flex-direction: column;
gap: 16px;
}
.auth-guest {
margin-top: 24px;
padding-top: 24px;
border-top: 1px solid var(--glass-border);
text-align: center;
display: flex;
flex-direction: column;
gap: 12px;
color: var(--text-muted);
}
.user-info {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
margin-bottom: 24px;
padding: 12px;
background: var(--glass-bg);
border-radius: 8px;
}
.role-badge {
padding: 4px 8px;
background: linear-gradient(135deg, #f59e0b, #d97706);
border-radius: 4px;
font-size: 10px;
font-weight: 700;
letter-spacing: 1px;
}
.btn-small {
padding: 6px 12px;
font-size: 12px;
}
/* =============================================================================
АДМИН-ПАНЕЛЬ
============================================================================= */
.admin-container {
max-width: 900px;
max-height: 90vh;
overflow-y: auto;
}
.admin-tabs {
display: flex;
gap: 8px;
margin-bottom: 24px;
flex-wrap: wrap;
}
.admin-tab-content {
padding: 16px 0;
}
.admin-tab-content h3 {
margin-bottom: 16px;
color: var(--text-primary);
}
/* Статистика логов */
.log-stats {
display: flex;
gap: 16px;
margin-bottom: 24px;
}
.stat-item {
flex: 1;
padding: 16px;
background: var(--glass-bg);
border-radius: 8px;
text-align: center;
}
.stat-value {
display: block;
font-size: 32px;
font-weight: 700;
color: var(--accent-primary);
}
.stat-label {
color: var(--text-muted);
font-size: 12px;
}
/* Фильтры логов */
.log-filters {
display: flex;
gap: 12px;
margin-bottom: 16px;
}
.log-filters .input {
flex: 1;
}
/* Таблица логов */
.log-table-container {
overflow-x: auto;
margin-bottom: 16px;
max-height: 400px;
overflow-y: auto;
}
.log-table {
width: 100%;
border-collapse: collapse;
}
.log-table th,
.log-table td {
padding: 12px 8px;
text-align: left;
border-bottom: 1px solid var(--glass-border);
}
.log-table th {
background: var(--bg-tertiary);
color: var(--text-secondary);
font-weight: 500;
position: sticky;
top: 0;
}
.log-table tr:hover {
background: var(--glass-bg);
}
.log-action-badge {
padding: 4px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 500;
}
.log-action-badge.login { background: var(--success); color: white; }
.log-action-badge.register { background: var(--info); color: white; }
.log-action-badge.room_created { background: var(--accent-primary); color: white; }
.log-action-badge.chat_message { background: var(--warning); color: black; }
.log-action-badge.game_action { background: var(--danger); color: white; }
/* Пагинация */
.log-pagination {
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
}
#log-page-info {
color: var(--text-muted);
}
/* LLM статус */
.llm-status-box {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
background: var(--glass-bg);
border-radius: 8px;
margin-top: 24px;
}
.llm-status-box.enabled {
border: 1px solid var(--success);
}
.llm-status-box.disabled {
border: 1px solid var(--danger);
opacity: 0.7;
}
.llm-status-icon {
font-size: 24px;
}
/* Select стили */
select.input {
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23fff' d='M6 8L1 3h10z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 12px center;
padding-right: 36px;
}
/* Анимация появления */
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.admin-tab-content {
animation: fadeInUp 0.3s ease;
}

213
server.js
View File

@ -9,14 +9,179 @@ const express = require('express');
const { WebSocketServer } = require('ws');
const http = require('http');
const path = require('path');
const database = require('./database');
const app = express();
const server = http.createServer(app);
const wss = new WebSocketServer({ server });
// Middleware для JSON
app.use(express.json());
// Статические файлы
app.use(express.static(path.join(__dirname, 'public')));
// =============================================================================
// REST API - АВТОРИЗАЦИЯ
// =============================================================================
/**
* Middleware проверки авторизации
*/
function authMiddleware(req, res, next) {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ error: 'Требуется авторизация' });
}
const decoded = database.verifyToken(token);
if (!decoded) {
return res.status(401).json({ error: 'Недействительный токен' });
}
req.user = decoded;
next();
}
/**
* Middleware проверки админа
*/
function adminMiddleware(req, res, next) {
if (req.user.role !== 'admin') {
return res.status(403).json({ error: 'Требуются права администратора' });
}
next();
}
// Регистрация
app.post('/api/auth/register', (req, res) => {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({ error: 'Введите логин и пароль' });
}
if (username.length < 3 || password.length < 6) {
return res.status(400).json({ error: 'Логин мин. 3 символа, пароль мин. 6' });
}
const result = database.registerUser(username, password);
if (result.success) {
res.json(result);
} else {
res.status(400).json({ error: result.error });
}
});
// Вход
app.post('/api/auth/login', (req, res) => {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({ error: 'Введите логин и пароль' });
}
const result = database.loginUser(username, password);
if (result.success) {
res.json(result);
} else {
res.status(401).json({ error: result.error });
}
});
// Проверка токена
app.get('/api/auth/me', authMiddleware, (req, res) => {
const user = database.getUserById(req.user.userId);
if (user) {
res.json({ user });
} else {
res.status(404).json({ error: 'Пользователь не найден' });
}
});
// =============================================================================
// REST API - НАСТРОЙКИ
// =============================================================================
// Получить пользовательские настройки
app.get('/api/settings/user', authMiddleware, (req, res) => {
const settings = database.getUserSettings(req.user.userId);
res.json({ settings });
});
// Сохранить пользовательские настройки
app.post('/api/settings/user', authMiddleware, (req, res) => {
database.saveUserSettings(req.user.userId, req.body);
database.logAction(req.user.userId, req.user.username, 'settings_update', req.body);
res.json({ success: true });
});
// Получить админские настройки
app.get('/api/settings/admin', authMiddleware, adminMiddleware, (req, res) => {
const settings = database.getAdminSettings();
res.json({ settings });
});
// Сохранить админские настройки
app.post('/api/settings/admin', authMiddleware, adminMiddleware, (req, res) => {
database.saveAdminSettings(req.body, req.user.userId);
database.logAction(req.user.userId, req.user.username, 'admin_settings_update', req.body);
res.json({ success: true });
});
// Получить публичные настройки (LLM статус для всех)
app.get('/api/settings/public', (req, res) => {
const adminSettings = database.getAdminSettings();
res.json({
llmEnabled: adminSettings.llmEnabled,
llmProvider: adminSettings.llmProvider,
llmApiUrl: adminSettings.llmApiUrl,
llmModel: adminSettings.llmModel
});
});
// =============================================================================
// REST API - ЛОГИ (только для админа)
// =============================================================================
// Получить логи
app.get('/api/logs', authMiddleware, adminMiddleware, (req, res) => {
const { limit, offset, actionType, userId, startDate, endDate } = req.query;
const logs = database.getLogs({
limit: parseInt(limit) || 100,
offset: parseInt(offset) || 0,
actionType,
userId,
startDate,
endDate
});
res.json({ logs });
});
// Статистика логов
app.get('/api/logs/stats', authMiddleware, adminMiddleware, (req, res) => {
const stats = database.getLogStats();
res.json({ stats });
});
// =============================================================================
// REST API - ЧАТ
// =============================================================================
// Получить историю чата комнаты
app.get('/api/chat/:roomId', authMiddleware, (req, res) => {
const { roomId } = req.params;
const { limit } = req.query;
const messages = database.getChatHistory(roomId, parseInt(limit) || 50);
res.json({ messages });
});
// =============================================================================
// ИГРОВЫЕ СТРУКТУРЫ ДАННЫХ
// =============================================================================
@ -898,6 +1063,15 @@ function handleCreateRoom(ws, message) {
connection.roomId = roomId;
connection.playerName = player.name;
// Логируем создание комнаты
database.logAction(
connection.userId || playerId,
player.name,
'room_created',
{ roomId, roomName, smallBlind, bigBlind, maxPlayers },
roomId
);
// Отправляем подтверждение
ws.send(JSON.stringify({
type: 'room_joined',
@ -1094,6 +1268,24 @@ function handleChat(ws, message) {
room.messages.push(chatMessage);
// Сохраняем сообщение в БД
database.saveChatMessage(
connection.roomId,
'multiplayer',
connection.userId || connection.playerId,
connection.playerName,
message.message
);
// Логируем действие
database.logAction(
connection.userId || connection.playerId,
connection.playerName,
'chat_message',
{ message: message.message },
connection.roomId
);
broadcastToRoom(connection.roomId, chatMessage);
}
@ -1212,13 +1404,26 @@ function broadcastRoomList() {
// =============================================================================
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
console.log(`
// Асинхронный запуск с инициализацией БД
(async () => {
try {
// Инициализируем базу данных
await database.initDatabase();
server.listen(PORT, () => {
console.log(`
🃏 Texas Hold'em Poker Server 🃏
Сервер запущен на http://localhost:${PORT} ║
WebSocket: ws://localhost:${PORT} ║
База данных: SQLite (poker.db)
`);
});
`);
});
} catch (error) {
console.error('Ошибка запуска сервера:', error);
process.exit(1);
}
})();