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:
parent
c3ace2834d
commit
eec780d8af
|
|
@ -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
|
||||
};
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
15
package.json
15
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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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,13 +448,38 @@
|
|||
<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>
|
||||
<!-- 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 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>
|
||||
<span>Включить LLM чат для всех</span>
|
||||
<label class="switch">
|
||||
<input type="checkbox" id="setting-llm-enabled" onchange="updateSettings()">
|
||||
<input type="checkbox" id="admin-llm-enabled" onchange="saveAdminSettings()">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
|
@ -400,33 +487,85 @@
|
|||
|
||||
<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>
|
||||
<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 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">
|
||||
<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="llm-model" class="input" value="llama3.2" onchange="updateSettings()" placeholder="llama3.2, mistral, etc.">
|
||||
<input type="text" id="admin-llm-model" class="input" value="llama3.2" onchange="saveAdminSettings()">
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="llm-api-key-group" style="display: none;">
|
||||
<div class="form-group">
|
||||
<label>API Key (для OpenAI)</label>
|
||||
<input type="password" id="llm-api-key" class="input" onchange="updateSettings()" placeholder="sk-...">
|
||||
<input type="password" id="admin-llm-apikey" class="input" placeholder="sk-..." onchange="saveAdminSettings()">
|
||||
</div>
|
||||
|
||||
<button class="btn btn-secondary" id="test-llm-btn" onclick="testLLMConnection()">🔗 Проверить подключение</button>
|
||||
<button class="btn btn-secondary" onclick="testAdminLLM()">🔗 Проверить подключение</button>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 16px;"></div>
|
||||
<button class="btn btn-outline" onclick="resetSettings()">Сбросить настройки</button>
|
||||
<!-- Вкладка логов -->
|
||||
<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>
|
||||
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
205
server.js
205
server.js
|
|
@ -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,6 +1404,13 @@ function broadcastRoomList() {
|
|||
// =============================================================================
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
// Асинхронный запуск с инициализацией БД
|
||||
(async () => {
|
||||
try {
|
||||
// Инициализируем базу данных
|
||||
await database.initDatabase();
|
||||
|
||||
server.listen(PORT, () => {
|
||||
console.log(`
|
||||
╔════════════════════════════════════════════════════════════════╗
|
||||
|
|
@ -1219,6 +1418,12 @@ server.listen(PORT, () => {
|
|||
║ ║
|
||||
║ Сервер запущен на http://localhost:${PORT} ║
|
||||
║ WebSocket: ws://localhost:${PORT} ║
|
||||
║ База данных: SQLite (poker.db) ║
|
||||
╚════════════════════════════════════════════════════════════════╝
|
||||
`);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Ошибка запуска сервера:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
||||
|
|
|
|||
Loading…
Reference in New Issue