736 lines
21 KiB
JavaScript
736 lines
21 KiB
JavaScript
/**
|
|
* =============================================================================
|
|
* 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();
|
|
|
|
// Всегда проверяем и инициализируем админские настройки
|
|
initDefaultAdminSettings();
|
|
|
|
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',
|
|
banned INTEGER DEFAULT 0,
|
|
ip_address TEXT,
|
|
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
|
|
)
|
|
`);
|
|
|
|
// Миграция: добавляем поля banned и ip_address если их нет
|
|
try {
|
|
const columns = db.exec(`PRAGMA table_info(users)`);
|
|
const columnNames = columns[0]?.values.map(col => col[1]) || [];
|
|
|
|
if (!columnNames.includes('banned')) {
|
|
db.run(`ALTER TABLE users ADD COLUMN banned INTEGER DEFAULT 0`);
|
|
console.log('✅ Добавлено поле banned в таблицу users');
|
|
}
|
|
|
|
if (!columnNames.includes('ip_address')) {
|
|
db.run(`ALTER TABLE users ADD COLUMN ip_address TEXT`);
|
|
console.log('✅ Добавлено поле ip_address в таблицу users');
|
|
}
|
|
} catch (error) {
|
|
console.log('⚠️ Миграция таблицы users пропущена:', error.message);
|
|
}
|
|
|
|
// Настройки пользователей
|
|
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 initDefaultAdminSettings() {
|
|
const settingsCheck = db.exec("SELECT COUNT(*) as count FROM admin_settings");
|
|
const count = settingsCheck.length > 0 ? settingsCheck[0].values[0][0] : 0;
|
|
|
|
if (count === 0) {
|
|
const defaults = {
|
|
llmEnabled: 'false',
|
|
llmProvider: 'ollama',
|
|
llmApiUrl: 'http://localhost:11434',
|
|
llmModel: 'llama3.2',
|
|
llmApiKey: '',
|
|
serverUrl: 'ws://localhost:3000'
|
|
};
|
|
|
|
Object.entries(defaults).forEach(([key, value]) => {
|
|
db.run(`
|
|
INSERT INTO admin_settings (key, value, updated_at)
|
|
VALUES (?, ?, datetime('now'))
|
|
`, [key, value]);
|
|
});
|
|
|
|
saveDatabase();
|
|
console.log('⚙️ Инициализированы дефолтные админские настройки');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Сохранить БД на диск
|
|
*/
|
|
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, ipAddress = null) {
|
|
try {
|
|
const result = db.exec(`
|
|
SELECT id, username, password_hash, role, banned
|
|
FROM users
|
|
WHERE username = ?
|
|
`, [username]);
|
|
|
|
if (result.length === 0 || result[0].values.length === 0) {
|
|
return { success: false, error: 'Неверный логин или пароль' };
|
|
}
|
|
|
|
const [userId, storedUsername, passwordHash, role, banned] = result[0].values[0];
|
|
|
|
// Проверка на бан
|
|
if (banned === 1) {
|
|
return { success: false, error: 'Ваш аккаунт заблокирован' };
|
|
}
|
|
|
|
if (!bcrypt.compareSync(password, passwordHash)) {
|
|
return { success: false, error: 'Неверный логин или пароль' };
|
|
}
|
|
|
|
// Обновляем last_login и IP
|
|
if (ipAddress) {
|
|
db.run(`
|
|
UPDATE users SET last_login = datetime('now'), ip_address = ? WHERE id = ?
|
|
`, [ipAddress, userId]);
|
|
} else {
|
|
db.run(`
|
|
UPDATE users SET last_login = datetime('now') WHERE id = ?
|
|
`, [userId]);
|
|
}
|
|
saveDatabase();
|
|
|
|
// Логируем
|
|
logAction(userId, storedUsername, 'login', { username: storedUsername, ip: ipAddress });
|
|
|
|
// Генерируем токен
|
|
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();
|
|
}
|
|
|
|
// =============================================================================
|
|
// УПРАВЛЕНИЕ ПОЛЬЗОВАТЕЛЯМИ (АДМИН)
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Получить всех пользователей
|
|
*/
|
|
function getAllUsers() {
|
|
const result = db.exec(`
|
|
SELECT id, username, role, banned, ip_address, last_login, created_at,
|
|
total_games, total_wins, total_chips_won
|
|
FROM users
|
|
ORDER BY created_at DESC
|
|
`);
|
|
|
|
if (result.length === 0) return [];
|
|
|
|
return result[0].values.map(row => ({
|
|
id: row[0],
|
|
username: row[1],
|
|
role: row[2],
|
|
banned: row[3],
|
|
ipAddress: row[4],
|
|
lastLogin: row[5],
|
|
createdAt: row[6],
|
|
totalGames: row[7],
|
|
totalWins: row[8],
|
|
totalChipsWon: row[9]
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Получить пользователя по логину
|
|
*/
|
|
function getUserByUsername(username) {
|
|
const result = db.exec(`
|
|
SELECT id, username, role, banned, ip_address, last_login, created_at
|
|
FROM users
|
|
WHERE username = ?
|
|
`, [username]);
|
|
|
|
if (result.length === 0 || result[0].values.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
const [id, uname, role, banned, ipAddress, lastLogin, createdAt] = result[0].values[0];
|
|
return { id, username: uname, role, banned, ipAddress, lastLogin, createdAt };
|
|
}
|
|
|
|
/**
|
|
* Обновить пользователя
|
|
*/
|
|
function updateUser(username, updates) {
|
|
const user = getUserByUsername(username);
|
|
if (!user) {
|
|
throw new Error('Пользователь не найден');
|
|
}
|
|
|
|
const fields = [];
|
|
const values = [];
|
|
|
|
if (updates.role !== undefined) {
|
|
fields.push('role = ?');
|
|
values.push(updates.role);
|
|
}
|
|
|
|
if (updates.banned !== undefined) {
|
|
fields.push('banned = ?');
|
|
values.push(updates.banned);
|
|
}
|
|
|
|
if (updates.password) {
|
|
const hashedPassword = bcrypt.hashSync(updates.password, 10);
|
|
fields.push('password_hash = ?');
|
|
values.push(hashedPassword);
|
|
}
|
|
|
|
if (fields.length === 0) {
|
|
return; // Нечего обновлять
|
|
}
|
|
|
|
values.push(username);
|
|
|
|
db.run(`
|
|
UPDATE users
|
|
SET ${fields.join(', ')}
|
|
WHERE username = ?
|
|
`, values);
|
|
|
|
saveDatabase();
|
|
}
|
|
|
|
/**
|
|
* Удалить пользователя
|
|
*/
|
|
function deleteUser(username) {
|
|
db.run(`DELETE FROM users WHERE username = ?`, [username]);
|
|
db.run(`DELETE FROM user_settings WHERE user_id = (SELECT id FROM users WHERE username = ?)`, [username]);
|
|
saveDatabase();
|
|
}
|
|
|
|
// =============================================================================
|
|
// ЭКСПОРТ
|
|
// =============================================================================
|
|
|
|
module.exports = {
|
|
initDatabase,
|
|
saveDatabase,
|
|
|
|
// Пользователи
|
|
registerUser,
|
|
loginUser,
|
|
verifyToken,
|
|
getUserById,
|
|
getAllUsers,
|
|
getUserByUsername,
|
|
updateUser,
|
|
deleteUser,
|
|
|
|
// Настройки
|
|
getUserSettings,
|
|
saveUserSettings,
|
|
getAdminSettings,
|
|
saveAdminSettings,
|
|
|
|
// Логирование
|
|
logAction,
|
|
getLogs,
|
|
getLogStats,
|
|
|
|
// Чат
|
|
saveChatMessage,
|
|
getChatHistory,
|
|
clearRoomChat
|
|
};
|