Update files
This commit is contained in:
parent
a4bae70823
commit
d83399e5f9
|
|
@ -0,0 +1,145 @@
|
||||||
|
# Обновление: Управление админами и исправления
|
||||||
|
|
||||||
|
## ✅ Что сделано
|
||||||
|
|
||||||
|
### 1. Убран суффикс "Сообщите об ошибке" из специфичных ошибок
|
||||||
|
- Обновлён `backend/server.js`
|
||||||
|
- Суффикс не добавляется к ошибкам валидации, публикации и других операционных сообщений
|
||||||
|
- Список исключений: "Загрузите хотя бы одно изображение", "Не удалось опубликовать в канал", "Требуется авторизация", и др.
|
||||||
|
|
||||||
|
### 2. Добавлено управление админами через Mini App
|
||||||
|
**Новые модели:**
|
||||||
|
- `backend/models/AdminConfirmation.js` - хранение кодов подтверждения (TTL 5 минут)
|
||||||
|
- Обновлена `backend/models/ModerationAdmin.js` - добавлено поле `adminNumber` (1-10)
|
||||||
|
|
||||||
|
**Новые API endpoints в `/api/mod-app`:**
|
||||||
|
- `GET /admins` - получить список всех админов
|
||||||
|
- `POST /admins/initiate-add` - инициировать добавление админа (только для @glpshchn00)
|
||||||
|
- `POST /admins/confirm-add` - подтвердить добавление по коду
|
||||||
|
- `POST /admins/initiate-remove` - инициировать удаление админа (только для @glpshchn00)
|
||||||
|
- `POST /admins/confirm-remove` - подтвердить удаление по коду
|
||||||
|
|
||||||
|
**Как работает:**
|
||||||
|
1. Владелец (@glpshchn00) видит кнопки "Назначить" и "Снять" у пользователей
|
||||||
|
2. При нажатии выбирается номер админа (1-10)
|
||||||
|
3. Система генерирует 6-значный код и отправляет пользователю в личку бота
|
||||||
|
4. Пользователь вводит код в Mini App
|
||||||
|
5. После подтверждения админ добавляется/удаляется
|
||||||
|
|
||||||
|
### 3. Номера админов (1-10)
|
||||||
|
- Каждому админу присваивается уникальный номер от 1 до 10
|
||||||
|
- Номер выбирается владельцем при назначении
|
||||||
|
- Номер используется автоматически при публикации постов (теперь НЕ нужно выбирать слот)
|
||||||
|
|
||||||
|
### 4. Убран выбор слота из публикации
|
||||||
|
- В `backend/routes/modApp.js` роут `/channel/publish` обновлён
|
||||||
|
- Теперь автоматически берётся `adminNumber` из базы данных
|
||||||
|
- Поле `slot` больше не требуется в запросе
|
||||||
|
|
||||||
|
### 5. Исправлен live chat
|
||||||
|
- Обновлён `backend/websocket.js`
|
||||||
|
- Владелец (@glpshchn00) теперь может подключаться к чату
|
||||||
|
- Добавлена проверка `config.moderationOwnerUsernames`
|
||||||
|
- Улучшено логирование подключений
|
||||||
|
|
||||||
|
## 📦 Деплой
|
||||||
|
|
||||||
|
### На сервере:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /var/www/nakama
|
||||||
|
|
||||||
|
# 1. Обновить код (если через git)
|
||||||
|
git pull
|
||||||
|
|
||||||
|
# 2. Установить зависимости (если добавились новые)
|
||||||
|
npm install --production
|
||||||
|
|
||||||
|
# 3. Перезапустить бекэнд
|
||||||
|
pm2 restart nakama-backend --update-env
|
||||||
|
|
||||||
|
# 4. Проверить логи
|
||||||
|
pm2 logs nakama-backend --lines 50
|
||||||
|
```
|
||||||
|
|
||||||
|
### Обновление существующих админов:
|
||||||
|
|
||||||
|
Если у тебя уже есть админы в базе БЕЗ `adminNumber`, нужно добавить номера вручную:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mongosh nakama
|
||||||
|
```
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Посмотреть текущих админов
|
||||||
|
db.moderationadmins.find()
|
||||||
|
|
||||||
|
// Назначить номера вручную (замени ID и номера)
|
||||||
|
db.moderationadmins.updateOne(
|
||||||
|
{ _id: ObjectId("...") },
|
||||||
|
{ $set: { adminNumber: 1 } }
|
||||||
|
)
|
||||||
|
|
||||||
|
db.moderationadmins.updateOne(
|
||||||
|
{ _id: ObjectId("...") },
|
||||||
|
{ $set: { adminNumber: 2 } }
|
||||||
|
)
|
||||||
|
|
||||||
|
// И так далее для каждого админа
|
||||||
|
```
|
||||||
|
|
||||||
|
Или удалить всех и добавить заново через Mini App:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
db.moderationadmins.deleteMany({})
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 Следующие шаги
|
||||||
|
|
||||||
|
Нужно обновить фронтенд модерации (`moderation/frontend/src/App.jsx`), чтобы добавить:
|
||||||
|
|
||||||
|
1. **Новую вкладку "Админы"** с:
|
||||||
|
- Списком всех админов с номерами
|
||||||
|
- Кнопками "Назначить" и "Снять" (только для @glpshchn00)
|
||||||
|
- Модальным окном для ввода кода подтверждения
|
||||||
|
- Выбором номера админа (1-10)
|
||||||
|
|
||||||
|
2. **Убрать выбор слота** из вкладки "Публикация":
|
||||||
|
- Удалить dropdown со слотами
|
||||||
|
- Показывать текущий номер админа из базы
|
||||||
|
|
||||||
|
3. **Тестирование:**
|
||||||
|
- Проверить live chat
|
||||||
|
- Проверить добавление/удаление админов
|
||||||
|
- Проверить публикацию с автоматическим слотом
|
||||||
|
|
||||||
|
## 🔒 Безопасность
|
||||||
|
|
||||||
|
- Все операции с админами требуют авторизации через `authenticateModeration`
|
||||||
|
- Добавление/удаление доступно только владельцу через middleware `requireOwner`
|
||||||
|
- Коды подтверждения удаляются автоматически через 5 минут (MongoDB TTL)
|
||||||
|
- Коды одноразовые - удаляются сразу после использования
|
||||||
|
- Боту нужны права отправки сообщений пользователям
|
||||||
|
|
||||||
|
## ⚠️ Важно
|
||||||
|
|
||||||
|
**Перед запуском на проде убедись:**
|
||||||
|
1. `MODERATION_BOT_TOKEN` правильно настроен в `.env`
|
||||||
|
2. Бот может отправлять сообщения пользователям (они должны начать диалог с ботом)
|
||||||
|
3. Владелец (@glpshchn00) правильно указан в `MODERATION_OWNER_USERNAMES`
|
||||||
|
4. MongoDB доступна и работает
|
||||||
|
|
||||||
|
## 🐛 Возможные проблемы
|
||||||
|
|
||||||
|
**"Бот не отправляет код":**
|
||||||
|
- Проверь, что пользователь написал боту `/start`
|
||||||
|
- Проверь `MODERATION_BOT_TOKEN` в логах
|
||||||
|
|
||||||
|
**"Номер админа уже занят":**
|
||||||
|
- Проверь `db.moderationadmins.find()` - возможно есть дубликаты
|
||||||
|
- Очисти базу: `db.moderationadmins.deleteMany({})`
|
||||||
|
|
||||||
|
**"Live chat не подключается":**
|
||||||
|
- Проверь, что владелец указан в `MODERATION_OWNER_USERNAMES`
|
||||||
|
- Посмотри логи WebSocket подключения
|
||||||
|
|
||||||
|
|
@ -394,9 +394,31 @@ const sendChannelMediaGroup = async (files, caption) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Отправить сообщение пользователю
|
||||||
|
*/
|
||||||
|
const sendMessageToUser = async (userId, message) => {
|
||||||
|
if (!moderationBot) {
|
||||||
|
throw new Error('Бот модерации не инициализирован');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axios.post(`${TELEGRAM_API}/sendMessage`, {
|
||||||
|
chat_id: userId,
|
||||||
|
text: message,
|
||||||
|
parse_mode: 'HTML'
|
||||||
|
});
|
||||||
|
log('info', 'Сообщение отправлено пользователю', { userId });
|
||||||
|
} catch (error) {
|
||||||
|
log('error', 'Не удалось отправить сообщение пользователю', { userId, error: error.response?.data || error.message });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
startServerMonitorBot,
|
startServerMonitorBot,
|
||||||
sendChannelMediaGroup,
|
sendChannelMediaGroup,
|
||||||
|
sendMessageToUser,
|
||||||
isModerationAdmin
|
isModerationAdmin
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
const mongoose = require('mongoose');
|
||||||
|
|
||||||
|
const AdminConfirmationSchema = new mongoose.Schema({
|
||||||
|
userId: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
index: true
|
||||||
|
},
|
||||||
|
code: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
adminNumber: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
min: 1,
|
||||||
|
max: 10
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
type: String,
|
||||||
|
enum: ['add', 'remove'],
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
createdAt: {
|
||||||
|
type: Date,
|
||||||
|
default: Date.now,
|
||||||
|
expires: 300 // Удаляется через 5 минут
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = mongoose.model('AdminConfirmation', AdminConfirmationSchema);
|
||||||
|
|
||||||
|
|
@ -15,6 +15,13 @@ const ModerationAdminSchema = new mongoose.Schema({
|
||||||
},
|
},
|
||||||
firstName: String,
|
firstName: String,
|
||||||
lastName: String,
|
lastName: String,
|
||||||
|
adminNumber: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
min: 1,
|
||||||
|
max: 10,
|
||||||
|
unique: true
|
||||||
|
},
|
||||||
addedBy: {
|
addedBy: {
|
||||||
type: String,
|
type: String,
|
||||||
lowercase: true,
|
lowercase: true,
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,16 @@ const router = express.Router();
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const multer = require('multer');
|
const multer = require('multer');
|
||||||
|
const crypto = require('crypto');
|
||||||
const { authenticateModeration } = require('../middleware/auth');
|
const { authenticateModeration } = require('../middleware/auth');
|
||||||
const { logSecurityEvent } = require('../middleware/logger');
|
const { logSecurityEvent } = require('../middleware/logger');
|
||||||
const User = require('../models/User');
|
const User = require('../models/User');
|
||||||
const Post = require('../models/Post');
|
const Post = require('../models/Post');
|
||||||
const Report = require('../models/Report');
|
const Report = require('../models/Report');
|
||||||
|
const ModerationAdmin = require('../models/ModerationAdmin');
|
||||||
|
const AdminConfirmation = require('../models/AdminConfirmation');
|
||||||
const { listAdmins, isModerationAdmin, normalizeUsername } = require('../services/moderationAdmin');
|
const { listAdmins, isModerationAdmin, normalizeUsername } = require('../services/moderationAdmin');
|
||||||
const { sendChannelMediaGroup } = require('../bots/serverMonitor');
|
const { sendChannelMediaGroup, sendMessageToUser } = require('../bots/serverMonitor');
|
||||||
const config = require('../config');
|
const config = require('../config');
|
||||||
|
|
||||||
const TEMP_DIR = path.join(__dirname, '../uploads/mod-channel');
|
const TEMP_DIR = path.join(__dirname, '../uploads/mod-channel');
|
||||||
|
|
@ -44,6 +47,7 @@ const requireModerationAccess = async (req, res, next) => {
|
||||||
|
|
||||||
if (OWNER_USERNAMES.has(username) || req.user.role === 'admin') {
|
if (OWNER_USERNAMES.has(username) || req.user.role === 'admin') {
|
||||||
req.isModerationAdmin = true;
|
req.isModerationAdmin = true;
|
||||||
|
req.isOwner = true;
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -53,9 +57,17 @@ const requireModerationAccess = async (req, res, next) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
req.isModerationAdmin = true;
|
req.isModerationAdmin = true;
|
||||||
|
req.isOwner = false;
|
||||||
return next();
|
return next();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const requireOwner = (req, res, next) => {
|
||||||
|
if (!req.isOwner) {
|
||||||
|
return res.status(403).json({ error: 'Требуются права владельца' });
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
|
||||||
const serializeUser = (user) => ({
|
const serializeUser = (user) => ({
|
||||||
id: user._id,
|
id: user._id,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
|
|
@ -360,20 +372,289 @@ router.put('/reports/:id', authenticateModeration, requireModerationAccess, asyn
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ========== УПРАВЛЕНИЕ АДМИНАМИ ==========
|
||||||
|
|
||||||
|
// Получить список всех админов
|
||||||
|
router.get('/admins', authenticateModeration, requireModerationAccess, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const admins = await ModerationAdmin.find().sort({ adminNumber: 1 });
|
||||||
|
res.json({
|
||||||
|
admins: admins.map(admin => ({
|
||||||
|
id: admin._id,
|
||||||
|
telegramId: admin.telegramId,
|
||||||
|
username: admin.username,
|
||||||
|
firstName: admin.firstName,
|
||||||
|
lastName: admin.lastName,
|
||||||
|
adminNumber: admin.adminNumber,
|
||||||
|
addedBy: admin.addedBy,
|
||||||
|
createdAt: admin.createdAt
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка получения списка админов:', error);
|
||||||
|
res.status(500).json({ error: 'Ошибка получения списка админов' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Инициировать добавление админа (только для владельца)
|
||||||
|
router.post('/admins/initiate-add', authenticateModeration, requireModerationAccess, requireOwner, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { userId, adminNumber } = req.body;
|
||||||
|
|
||||||
|
if (!userId || !adminNumber) {
|
||||||
|
return res.status(400).json({ error: 'Не указан ID пользователя или номер админа' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (adminNumber < 1 || adminNumber > 10) {
|
||||||
|
return res.status(400).json({ error: 'Номер админа должен быть от 1 до 10' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверить, не занят ли номер
|
||||||
|
const existingAdmin = await ModerationAdmin.findOne({ adminNumber });
|
||||||
|
if (existingAdmin) {
|
||||||
|
return res.status(400).json({ error: 'Номер админа уже занят' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверить, существует ли пользователь
|
||||||
|
const user = await User.findById(userId);
|
||||||
|
if (!user) {
|
||||||
|
return res.status(404).json({ error: 'Пользователь не найден' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверить, не является ли пользователь уже админом
|
||||||
|
const isAlreadyAdmin = await ModerationAdmin.findOne({ telegramId: user.telegramId });
|
||||||
|
if (isAlreadyAdmin) {
|
||||||
|
return res.status(400).json({ error: 'Пользователь уже является админом' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Генерировать 6-значный код
|
||||||
|
const code = crypto.randomInt(100000, 999999).toString();
|
||||||
|
|
||||||
|
// Сохранить код подтверждения
|
||||||
|
await AdminConfirmation.create({
|
||||||
|
userId: user.telegramId,
|
||||||
|
code,
|
||||||
|
adminNumber,
|
||||||
|
action: 'add'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Отправить код пользователю
|
||||||
|
await sendMessageToUser(
|
||||||
|
user.telegramId,
|
||||||
|
`<b>Подтверждение назначения админом</b>\n\n` +
|
||||||
|
`Вас назначают администратором модерации.\n` +
|
||||||
|
`Номер админа: <b>${adminNumber}</b>\n\n` +
|
||||||
|
`Для подтверждения введите код в приложении:\n` +
|
||||||
|
`<code>${code}</code>\n\n` +
|
||||||
|
`Код действителен 5 минут.`
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Код подтверждения отправлен пользователю',
|
||||||
|
username: user.username
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка инициирования добавления админа:', error);
|
||||||
|
res.status(500).json({ error: 'Ошибка отправки кода подтверждения' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Подтвердить добавление админа
|
||||||
|
router.post('/admins/confirm-add', authenticateModeration, requireModerationAccess, requireOwner, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { userId, code } = req.body;
|
||||||
|
|
||||||
|
if (!userId || !code) {
|
||||||
|
return res.status(400).json({ error: 'Не указан ID пользователя или код' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await User.findById(userId);
|
||||||
|
if (!user) {
|
||||||
|
return res.status(404).json({ error: 'Пользователь не найден' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Найти код подтверждения
|
||||||
|
const confirmation = await AdminConfirmation.findOne({
|
||||||
|
userId: user.telegramId,
|
||||||
|
code,
|
||||||
|
action: 'add'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!confirmation) {
|
||||||
|
return res.status(400).json({ error: 'Неверный код подтверждения' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверить, не занят ли номер
|
||||||
|
const existingAdmin = await ModerationAdmin.findOne({ adminNumber: confirmation.adminNumber });
|
||||||
|
if (existingAdmin) {
|
||||||
|
await AdminConfirmation.deleteOne({ _id: confirmation._id });
|
||||||
|
return res.status(400).json({ error: 'Номер админа уже занят' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавить админа
|
||||||
|
const newAdmin = await ModerationAdmin.create({
|
||||||
|
telegramId: user.telegramId,
|
||||||
|
username: normalizeUsername(user.username),
|
||||||
|
firstName: user.firstName,
|
||||||
|
lastName: user.lastName,
|
||||||
|
adminNumber: confirmation.adminNumber,
|
||||||
|
addedBy: normalizeUsername(req.user.username)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Удалить код подтверждения
|
||||||
|
await AdminConfirmation.deleteOne({ _id: confirmation._id });
|
||||||
|
|
||||||
|
// Уведомить пользователя
|
||||||
|
try {
|
||||||
|
await sendMessageToUser(
|
||||||
|
user.telegramId,
|
||||||
|
`<b>✅ Вы назначены администратором модерации!</b>\n\n` +
|
||||||
|
`Ваш номер: <b>${confirmation.adminNumber}</b>\n` +
|
||||||
|
`Теперь вы можете использовать модераторское приложение.`
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Не удалось отправить уведомление пользователю:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
admin: {
|
||||||
|
id: newAdmin._id,
|
||||||
|
telegramId: newAdmin.telegramId,
|
||||||
|
username: newAdmin.username,
|
||||||
|
firstName: newAdmin.firstName,
|
||||||
|
lastName: newAdmin.lastName,
|
||||||
|
adminNumber: newAdmin.adminNumber
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка подтверждения добавления админа:', error);
|
||||||
|
res.status(500).json({ error: 'Ошибка добавления админа' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Инициировать удаление админа (только для владельца)
|
||||||
|
router.post('/admins/initiate-remove', authenticateModeration, requireModerationAccess, requireOwner, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { adminId } = req.body;
|
||||||
|
|
||||||
|
if (!adminId) {
|
||||||
|
return res.status(400).json({ error: 'Не указан ID админа' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const admin = await ModerationAdmin.findById(adminId);
|
||||||
|
if (!admin) {
|
||||||
|
return res.status(404).json({ error: 'Администратор не найден' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Генерировать 6-значный код
|
||||||
|
const code = crypto.randomInt(100000, 999999).toString();
|
||||||
|
|
||||||
|
// Сохранить код подтверждения
|
||||||
|
await AdminConfirmation.create({
|
||||||
|
userId: admin.telegramId,
|
||||||
|
code,
|
||||||
|
adminNumber: admin.adminNumber,
|
||||||
|
action: 'remove'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Отправить код пользователю
|
||||||
|
await sendMessageToUser(
|
||||||
|
admin.telegramId,
|
||||||
|
`<b>Подтверждение снятия с должности админа</b>\n\n` +
|
||||||
|
`Вас снимают с должности администратора модерации.\n\n` +
|
||||||
|
`Для подтверждения введите код в приложении:\n` +
|
||||||
|
`<code>${code}</code>\n\n` +
|
||||||
|
`Код действителен 5 минут.`
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Код подтверждения отправлен админу',
|
||||||
|
username: admin.username
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка инициирования удаления админа:', error);
|
||||||
|
res.status(500).json({ error: 'Ошибка отправки кода подтверждения' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Подтвердить удаление админа
|
||||||
|
router.post('/admins/confirm-remove', authenticateModeration, requireModerationAccess, requireOwner, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { adminId, code } = req.body;
|
||||||
|
|
||||||
|
if (!adminId || !code) {
|
||||||
|
return res.status(400).json({ error: 'Не указан ID админа или код' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const admin = await ModerationAdmin.findById(adminId);
|
||||||
|
if (!admin) {
|
||||||
|
return res.status(404).json({ error: 'Администратор не найден' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Найти код подтверждения
|
||||||
|
const confirmation = await AdminConfirmation.findOne({
|
||||||
|
userId: admin.telegramId,
|
||||||
|
code,
|
||||||
|
action: 'remove'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!confirmation) {
|
||||||
|
return res.status(400).json({ error: 'Неверный код подтверждения' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Удалить админа
|
||||||
|
await ModerationAdmin.deleteOne({ _id: admin._id });
|
||||||
|
|
||||||
|
// Удалить код подтверждения
|
||||||
|
await AdminConfirmation.deleteOne({ _id: confirmation._id });
|
||||||
|
|
||||||
|
// Уведомить пользователя
|
||||||
|
try {
|
||||||
|
await sendMessageToUser(
|
||||||
|
admin.telegramId,
|
||||||
|
`<b>❌ Вы сняты с должности администратора модерации</b>\n\n` +
|
||||||
|
`Доступ к модераторскому приложению прекращён.`
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Не удалось отправить уведомление пользователю:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка подтверждения удаления админа:', error);
|
||||||
|
res.status(500).json({ error: 'Ошибка удаления админа' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ========== ПУБЛИКАЦИЯ В КАНАЛ ==========
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/channel/publish',
|
'/channel/publish',
|
||||||
authenticateModeration,
|
authenticateModeration,
|
||||||
requireModerationAccess,
|
requireModerationAccess,
|
||||||
upload.array('images', 10),
|
upload.array('images', 10),
|
||||||
async (req, res) => {
|
async (req, res) => {
|
||||||
const { description = '', tags, slot } = req.body;
|
const { description = '', tags } = req.body;
|
||||||
const files = req.files || [];
|
const files = req.files || [];
|
||||||
|
|
||||||
if (!files.length) {
|
if (!files.length) {
|
||||||
return res.status(400).json({ error: 'Загрузите хотя бы одно изображение' });
|
return res.status(400).json({ error: 'Загрузите хотя бы одно изображение' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const slotNumber = Math.max(Math.min(parseInt(slot, 10) || 1, 10), 1);
|
// Получить номер админа из базы
|
||||||
|
const admin = await ModerationAdmin.findOne({ telegramId: req.user.telegramId });
|
||||||
|
|
||||||
|
// Проверить, что админ имеет номер от 1 до 10
|
||||||
|
if (!admin || !admin.adminNumber || admin.adminNumber < 1 || admin.adminNumber > 10) {
|
||||||
|
return res.status(403).json({
|
||||||
|
error: 'Публиковать в канал могут только админы с номерами от 1 до 10. Обратитесь к владельцу для назначения номера.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const slotNumber = admin.adminNumber;
|
||||||
|
|
||||||
let tagsArray = [];
|
let tagsArray = [];
|
||||||
if (typeof tags === 'string' && tags.trim()) {
|
if (typeof tags === 'string' && tags.trim()) {
|
||||||
|
|
|
||||||
|
|
@ -80,20 +80,40 @@ app.use((req, res, next) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof obj.error === 'string' && !obj.error.includes(ERROR_SUPPORT_SUFFIX)) {
|
// Список ошибок, к которым НЕ нужно добавлять суффикс
|
||||||
|
const skipSuffixMessages = [
|
||||||
|
'Загрузите хотя бы одно изображение',
|
||||||
|
'Не удалось опубликовать в канал',
|
||||||
|
'Публиковать в канал могут только админы',
|
||||||
|
'Требуется авторизация',
|
||||||
|
'Требуются права',
|
||||||
|
'Неверный код подтверждения',
|
||||||
|
'Код подтверждения истёк',
|
||||||
|
'Номер админа уже занят',
|
||||||
|
'Пользователь не найден',
|
||||||
|
'Администратор не найден'
|
||||||
|
];
|
||||||
|
|
||||||
|
const shouldSkipSuffix = (text) => {
|
||||||
|
if (!text || typeof text !== 'string') return false;
|
||||||
|
return skipSuffixMessages.some(msg => text.includes(msg));
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof obj.error === 'string' && !obj.error.includes(ERROR_SUPPORT_SUFFIX) && !shouldSkipSuffix(obj.error)) {
|
||||||
obj.error += ERROR_SUPPORT_SUFFIX;
|
obj.error += ERROR_SUPPORT_SUFFIX;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof obj.message === 'string' && res.statusCode >= 400 && !obj.message.includes(ERROR_SUPPORT_SUFFIX)) {
|
if (typeof obj.message === 'string' && res.statusCode >= 400 && !obj.message.includes(ERROR_SUPPORT_SUFFIX) && !shouldSkipSuffix(obj.message)) {
|
||||||
obj.message += ERROR_SUPPORT_SUFFIX;
|
obj.message += ERROR_SUPPORT_SUFFIX;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(obj.errors)) {
|
if (Array.isArray(obj.errors)) {
|
||||||
obj.errors = obj.errors.map((item) => {
|
obj.errors = obj.errors.map((item) => {
|
||||||
if (typeof item === 'string') {
|
if (typeof item === 'string') {
|
||||||
|
if (shouldSkipSuffix(item)) return item;
|
||||||
return item.includes(ERROR_SUPPORT_SUFFIX) ? item : `${item}${ERROR_SUPPORT_SUFFIX}`;
|
return item.includes(ERROR_SUPPORT_SUFFIX) ? item : `${item}${ERROR_SUPPORT_SUFFIX}`;
|
||||||
}
|
}
|
||||||
if (item && typeof item === 'object' && typeof item.message === 'string' && !item.message.includes(ERROR_SUPPORT_SUFFIX)) {
|
if (item && typeof item === 'object' && typeof item.message === 'string' && !item.message.includes(ERROR_SUPPORT_SUFFIX) && !shouldSkipSuffix(item.message)) {
|
||||||
return { ...item, message: `${item.message}${ERROR_SUPPORT_SUFFIX}` };
|
return { ...item, message: `${item.message}${ERROR_SUPPORT_SUFFIX}` };
|
||||||
}
|
}
|
||||||
return item;
|
return item;
|
||||||
|
|
|
||||||
|
|
@ -68,12 +68,20 @@ function registerModerationChat() {
|
||||||
const telegramId = payload.telegramId;
|
const telegramId = payload.telegramId;
|
||||||
|
|
||||||
if (!username || !telegramId) {
|
if (!username || !telegramId) {
|
||||||
|
log('warn', 'Mod chat auth failed: no username/telegramId', { username, telegramId });
|
||||||
socket.emit('unauthorized');
|
socket.emit('unauthorized');
|
||||||
return socket.disconnect(true);
|
return socket.disconnect(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
const allowed = await isModerationAdmin({ username, telegramId });
|
// Проверить, является ли владельцем
|
||||||
if (!allowed) {
|
const ownerUsernames = config.moderationOwnerUsernames || [];
|
||||||
|
const isOwner = ownerUsernames.includes(username);
|
||||||
|
|
||||||
|
// Проверить, является ли админом
|
||||||
|
const isAdmin = await isModerationAdmin({ username, telegramId });
|
||||||
|
|
||||||
|
if (!isOwner && !isAdmin) {
|
||||||
|
log('warn', 'Mod chat access denied', { username, telegramId });
|
||||||
socket.emit('unauthorized');
|
socket.emit('unauthorized');
|
||||||
return socket.disconnect(true);
|
return socket.disconnect(true);
|
||||||
}
|
}
|
||||||
|
|
@ -81,11 +89,15 @@ function registerModerationChat() {
|
||||||
socket.data.authorized = true;
|
socket.data.authorized = true;
|
||||||
socket.data.username = username;
|
socket.data.username = username;
|
||||||
socket.data.telegramId = telegramId;
|
socket.data.telegramId = telegramId;
|
||||||
|
socket.data.isOwner = isOwner;
|
||||||
|
|
||||||
connectedModerators.set(socket.id, {
|
connectedModerators.set(socket.id, {
|
||||||
username,
|
username,
|
||||||
telegramId
|
telegramId,
|
||||||
|
isOwner
|
||||||
});
|
});
|
||||||
|
|
||||||
|
log('info', 'Mod chat auth success', { username, isOwner, isAdmin });
|
||||||
socket.emit('ready');
|
socket.emit('ready');
|
||||||
broadcastOnline();
|
broadcastOnline();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -131,10 +131,7 @@ export default function App() {
|
||||||
} finally {
|
} finally {
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
const telegramApp = window.Telegram?.WebApp;
|
// Убрана кнопка "Закрыть"
|
||||||
telegramApp?.MainButton?.setText?.('Закрыть');
|
|
||||||
telegramApp?.MainButton?.show?.();
|
|
||||||
telegramApp?.MainButton?.onClick?.(() => telegramApp.close());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -143,7 +140,6 @@ export default function App() {
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
window.Telegram?.WebApp?.MainButton?.hide?.();
|
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
@ -645,11 +641,6 @@ export default function App() {
|
||||||
<h1>Nakama Moderation</h1>
|
<h1>Nakama Moderation</h1>
|
||||||
<span className="subtitle">@{user.username}</span>
|
<span className="subtitle">@{user.username}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="header-actions">
|
|
||||||
<button className="btn" onClick={() => window.Telegram?.WebApp?.close()}>
|
|
||||||
Закрыть
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<nav className="tabbar">
|
<nav className="tabbar">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue