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 @@ - - -

🤖 Настройки LLM чата

- -
-
- Включить LLM чат - -
+ +
+ 🤖 + LLM чат: загрузка...
-
- - -
- - - -
-
- -
- - -
- -
- - -
- - - - -
+ +
+
+ +

👑 Админ-панель

+ +
+ + + +
+ + +
+

🤖 Настройки LLM чата

+ +
+
+ Включить LLM чат для всех + +
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+ + + + + + +
+
+