diff --git a/database.js b/database.js
new file mode 100644
index 0000000..925259c
--- /dev/null
+++ b/database.js
@@ -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
+};
diff --git a/package-lock.json b/package-lock.json
index 92de514..4613a35 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index 3ac7e0e..4fe7cc1 100644
--- a/package.json
+++ b/package.json
@@ -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"
}
diff --git a/poker.db b/poker.db
new file mode 100644
index 0000000..49c6cb4
Binary files /dev/null and b/poker.db differ
diff --git a/public/auth.js b/public/auth.js
new file mode 100644
index 0000000..6a11a81
--- /dev/null
+++ b/public/auth.js
@@ -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 = '
| Логи не найдены |
';
+ 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 = `
+ ${time} |
+ ${log.username || 'Гость'} |
+ ${log.actionType} |
+ ${details} |
+ `;
+
+ 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;
diff --git a/public/index.html b/public/index.html
index fb62032..1c32e0a 100644
--- a/public/index.html
+++ b/public/index.html
@@ -8,8 +8,8 @@
-
-
+
+
+
+
+
+
+
@@ -386,50 +448,127 @@
- 🤖 Настройки LLM чата
-
-
-
- Включить LLM чат
-
-
+
+
+ 🤖
+ LLM чат: загрузка...
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
👑 Админ-панель
+
+
+
+
+
+
+
+
+
+
🤖 Настройки LLM чата
+
+
+
+ Включить LLM чат для всех
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
📋 История действий
+
+
+
+ 0
+ Всего
+
+
+ 0
+ Сегодня
+
+
+
+
+
+
+
+
+
+
+
+
+ | Время |
+ Пользователь |
+ Действие |
+ Детали |
+
+
+
+
+
+
+
+
+
+
+
+
+
👥 Пользователи
+
Раздел в разработке...
+
+
+
+