Update files

This commit is contained in:
glpshchn 2025-12-15 02:45:41 +03:00
parent e367f46d9f
commit f5c16a350d
33 changed files with 3832 additions and 8 deletions

View File

@ -100,8 +100,19 @@ router.post('/send-code', codeLimiter, async (req, res) => {
} catch (emailError) {
console.error('Ошибка отправки email:', emailError);
await EmailVerificationCode.deleteOne({ _id: verificationCode._id });
let errorMessage = 'Не удалось отправить код на email.';
if (emailError.code === 'EAUTH' || emailError.message?.includes('Authentication credentials invalid')) {
errorMessage = 'Ошибка аутентификации SMTP. Проверьте YANDEX_SMTP_USER и YANDEX_SMTP_PASSWORD в .env. Для Yandex используйте пароль приложения, а не основной пароль.';
} else if (emailError.code === 'ECONNECTION') {
errorMessage = 'Не удалось подключиться к SMTP серверу. Проверьте YANDEX_SMTP_HOST и YANDEX_SMTP_PORT.';
} else if (emailError.message) {
errorMessage = `Ошибка отправки email: ${emailError.message}`;
}
return res.status(500).json({
error: 'Не удалось отправить код на email. Проверьте настройки email сервера.'
error: errorMessage
});
}
} catch (error) {

View File

@ -53,15 +53,55 @@ const initializeEmailService = () => {
} else if (emailProvider === 'yandex' || emailProvider === 'smtp') {
const emailConfig = config.email?.[emailProvider] || config.email?.smtp || {};
transporter = nodemailer.createTransport({
host: emailConfig.host || process.env.SMTP_HOST,
port: emailConfig.port || parseInt(process.env.SMTP_PORT || '587', 10),
secure: emailConfig.secure === true || process.env.SMTP_SECURE === 'true',
auth: {
user: emailConfig.user || process.env.SMTP_USER,
pass: emailConfig.password || process.env.SMTP_PASSWORD
const smtpHost = emailConfig.host || process.env.SMTP_HOST || process.env.YANDEX_SMTP_HOST;
const smtpPort = emailConfig.port || parseInt(process.env.SMTP_PORT || process.env.YANDEX_SMTP_PORT || '587', 10);
const smtpSecure = emailConfig.secure !== undefined ? emailConfig.secure :
(process.env.SMTP_SECURE === 'true' || process.env.YANDEX_SMTP_SECURE === 'true' || smtpPort === 465);
const smtpUser = emailConfig.user || process.env.SMTP_USER || process.env.YANDEX_SMTP_USER;
const smtpPassword = emailConfig.password || process.env.SMTP_PASSWORD || process.env.YANDEX_SMTP_PASSWORD;
console.log('[Email] Настройка SMTP:', {
provider: emailProvider,
host: smtpHost,
port: smtpPort,
secure: smtpSecure,
user: smtpUser ? `${smtpUser.substring(0, 3)}***` : 'не указан',
hasPassword: !!smtpPassword,
envVars: {
YANDEX_SMTP_HOST: !!process.env.YANDEX_SMTP_HOST,
YANDEX_SMTP_USER: !!process.env.YANDEX_SMTP_USER,
YANDEX_SMTP_PASSWORD: !!process.env.YANDEX_SMTP_PASSWORD,
SMTP_HOST: !!process.env.SMTP_HOST,
SMTP_USER: !!process.env.SMTP_USER,
SMTP_PASSWORD: !!process.env.SMTP_PASSWORD
}
});
if (!smtpHost || !smtpUser || !smtpPassword) {
console.error('[Email] Неполная конфигурация SMTP:', {
hasHost: !!smtpHost,
hasUser: !!smtpUser,
hasPassword: !!smtpPassword,
emailConfig: emailConfig,
configEmail: config.email
});
throw new Error('SMTP конфигурация неполная. Проверьте настройки в .env. Для Yandex используйте YANDEX_SMTP_* переменные.');
}
transporter = nodemailer.createTransport({
host: smtpHost,
port: smtpPort,
secure: smtpSecure,
auth: {
user: smtpUser,
pass: smtpPassword
},
tls: {
rejectUnauthorized: false // Для отладки, в production лучше true
}
});
console.log('[Email] SMTP transporter создан успешно');
}
};
@ -204,6 +244,16 @@ const sendEmail = async (to, subject, html, text) => {
}
} catch (error) {
console.error('Ошибка отправки email:', error);
// Более информативные сообщения об ошибках
if (error.code === 'EAUTH') {
throw new Error('Неверные учетные данные SMTP. Проверьте YANDEX_SMTP_USER и YANDEX_SMTP_PASSWORD в .env файле. Для Yandex используйте пароль приложения, а не основной пароль.');
} else if (error.code === 'ECONNECTION') {
throw new Error('Не удалось подключиться к SMTP серверу. Проверьте YANDEX_SMTP_HOST и YANDEX_SMTP_PORT.');
} else if (error.message && error.message.includes('Authentication credentials invalid')) {
throw new Error('Неверные учетные данные SMTP. Убедитесь, что используете пароль приложения для Yandex, а не основной пароль аккаунта.');
}
throw error;
}
};

View File

@ -0,0 +1,215 @@
# 🐍 Python Backend для модерации - Готов к использованию!
Бэкенд модерации полностью портирован на Python3 с FastAPI.
## ✅ Что сделано
1. **FastAPI сервер** - современный async Python фреймворк
2. **Email через SMTP** - нативная поддержка Yandex SMTP (без проблем AWS SDK)
3. **MongoDB** - async подключение через Motor
4. **JWT аутентификация** - совместимо с Node.js версией
5. **WebSocket чат** - Socket.IO для модераторов
6. **MinIO интеграция** - для хранения файлов
7. **API совместимость** - фронтенд работает без изменений
8. **Docker ready** - готовый Dockerfile
## 🚀 Быстрый старт
### Вариант 1: Автоматический (рекомендуется)
```bash
cd moderation/backend-py
./start.sh
```
### Вариант 2: Ручной
```bash
cd moderation/backend-py
# Создать venv
python3 -m venv venv
source venv/bin/activate
# Установить зависимости
pip install -r requirements.txt
# Запустить
python main.py
```
## ⚙️ Настройка
### 1. Email (ОБЯЗАТЕЛЬНО!)
В `nakama/.env` добавьте:
```env
EMAIL_PROVIDER=yandex
YANDEX_SMTP_HOST=smtp.yandex.ru
YANDEX_SMTP_PORT=465
YANDEX_SMTP_SECURE=true
YANDEX_SMTP_USER=ваш_email@yandex.ru
YANDEX_SMTP_PASSWORD=ваш_пароль_приложения
EMAIL_FROM=noreply@nakama.guru
OWNER_EMAIL=admin@example.com
```
**Получить пароль приложения Yandex:**
- https://id.yandex.ru/security
- "Пароли приложений" → Создать для "Почта"
### 2. Создать админа
```bash
mongosh nakama
```
```javascript
// Обновить пользователя
db.users.updateOne(
{ username: "ваш_username" },
{
$set: {
email: "ваш_email@yandex.ru",
emailVerified: true,
role: "admin"
}
}
);
```
### 3. Запустить
```bash
cd moderation/backend-py
python main.py
```
Должно появиться:
```
============================================================
🚀 Запуск сервера модерации (Python)...
✅ MongoDB подключена (база: nakama)
📚 Коллекций в базе: 10
============================================================
============================================================
✅ Сервер модерации запущен (Python)
🌐 API: http://0.0.0.0:3001/api
🔌 WebSocket: http://0.0.0.0:3001/socket.io
📦 MongoDB: 103.80.87.247:27017/nakama
============================================================
```
## 📝 Проверка
1. **Health check:**
```bash
curl http://localhost:3001/health
# {"status":"ok","service":"moderation","version":"2.0.0-python"}
```
2. **Config:**
```bash
curl http://localhost:3001/api/moderation-auth/config
```
3. **Отправка кода:**
Откройте фронтенд модерации, введите email - код должен прийти!
## 🔧 Решение проблем
### Email не отправляется
Проверьте логи:
```
[Email] Настройка SMTP: {provider: 'yandex', host: 'smtp.yandex.ru', port: 465, ...}
```
Если `user: 'не указан'` - проверьте `YANDEX_SMTP_USER` в `.env`.
### 403 при send-code
```javascript
// Проверьте роль пользователя
db.users.findOne({ email: "ваш_email@yandex.ru" })
// Должно быть role: "admin" или "moderator"
```
### MongoDB не подключается
```bash
# Проверьте доступность
mongosh "mongodb://103.80.87.247:27017/nakama"
```
## 🐳 Docker
```bash
cd moderation/backend-py
docker build -t nakama-moderation-py .
docker run -d -p 3001:3001 --env-file ../../.env nakama-moderation-py
```
## 📦 Production
### PM2
```bash
pm2 start "uvicorn main:app --host 0.0.0.0 --port 3001 --workers 2" \
--name moderation-backend-py \
--cwd $(pwd) \
--interpreter python3
pm2 save
```
### Systemd
Создайте `/etc/systemd/system/nakama-moderation-py.service`:
```ini
[Unit]
Description=Nakama Moderation Backend (Python)
After=network.target
[Service]
Type=simple
User=nakama
WorkingDirectory=/path/to/nakama/moderation/backend-py
Environment="PATH=/path/to/nakama/moderation/backend-py/venv/bin"
ExecStart=/path/to/nakama/moderation/backend-py/venv/bin/uvicorn main:app --host 0.0.0.0 --port 3001 --workers 2
Restart=always
[Install]
WantedBy=multi-user.target
```
```bash
sudo systemctl daemon-reload
sudo systemctl enable nakama-moderation-py
sudo systemctl start nakama-moderation-py
```
## 📚 Документация
- `README.md` - Полное описание
- `QUICKSTART.md` - Быстрый старт
- `INSTALL.md` - Подробная установка
- `MIGRATION.md` - Миграция с Node.js
## ✨ Преимущества
- ✅ Email работает из коробки
- ✅ Меньше зависимостей
- ✅ Лучшая типизация
- ✅ Async/await
- ✅ Совместимость с Node.js версией
- ✅ Та же MongoDB
- ✅ API полностью совместимо
Фронтенд работает без изменений!

View File

@ -0,0 +1,19 @@
__pycache__
*.pyc
*.pyo
*.pyd
.Python
*.so
*.egg
*.egg-info
dist
build
.env
.venv
venv
env
.git
.gitignore
*.md
.DS_Store

17
moderation/backend-py/.gitignore vendored Normal file
View File

@ -0,0 +1,17 @@
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
venv/
env/
ENV/
.venv
*.egg
*.egg-info/
dist/
build/
.env
.DS_Store
*.log

View File

@ -0,0 +1,273 @@
# 🐳 Docker запуск - Python Moderation Backend
## Быстрый запуск
```bash
cd moderation/backend-py
./docker-start.sh
```
Скрипт автоматически:
1. Соберет Docker образ
2. Остановит старый контейнер (если есть)
3. Запустит новый контейнер
4. Покажет логи
## Ручной запуск
### 1. Сборка образа
```bash
cd moderation/backend-py
docker build -t nakama-moderation-py .
```
### 2. Запуск контейнера
```bash
docker run -d \
--name nakama-moderation-py \
-p 3001:3001 \
--env-file ../../.env \
--restart unless-stopped \
nakama-moderation-py
```
### 3. Проверка
```bash
# Логи
docker logs nakama-moderation-py
# Логи в реальном времени
docker logs -f nakama-moderation-py
# Health check
curl http://localhost:3001/health
```
## Docker Compose
### Вариант 1: Отдельный запуск
```bash
cd moderation/backend-py
docker-compose -f docker-compose.moderation-py.yml up -d
```
### Вариант 2: Интеграция в основной docker-compose
Добавьте в корневой `docker-compose.yml`:
```yaml
moderation-backend-py:
build:
context: ./moderation/backend-py
dockerfile: Dockerfile
container_name: nakama-moderation-backend-py
restart: unless-stopped
ports:
- "127.0.0.1:3001:3001"
env_file:
- .env
networks:
- nakama-network
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:3001/health')"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
```
Затем:
```bash
# Из корня проекта
docker-compose up -d moderation-backend-py
```
## Управление контейнером
```bash
# Остановить
docker stop nakama-moderation-py
# Запустить
docker start nakama-moderation-py
# Перезапустить
docker restart nakama-moderation-py
# Удалить
docker rm -f nakama-moderation-py
# Логи
docker logs nakama-moderation-py
docker logs -f nakama-moderation-py # real-time
# Зайти в контейнер
docker exec -it nakama-moderation-py bash
# Проверить статус
docker ps | grep moderation-py
```
## Обновление
```bash
# Пересобрать образ
docker build -t nakama-moderation-py .
# Перезапустить контейнер
docker stop nakama-moderation-py
docker rm nakama-moderation-py
./docker-start.sh
```
## Проверка работы
```bash
# 1. Health check
curl http://localhost:3001/health
# 2. Config
curl http://localhost:3001/api/moderation-auth/config
# 3. Логи
docker logs --tail 50 nakama-moderation-py
# Должны увидеть:
# ============================================================
# 🚀 Запуск сервера модерации (Python)...
# ✅ MongoDB подключена (база: nakama)
# ============================================================
```
## Production deployment
### С PM2 (если Docker не используется)
```bash
cd moderation/backend-py
source venv/bin/activate
pm2 start "uvicorn main:app --host 0.0.0.0 --port 3001 --workers 2" \
--name moderation-backend-py \
--cwd $(pwd) \
--interpreter python3
pm2 save
pm2 startup
```
### С Docker в production
```bash
# Из корня проекта
docker-compose up -d moderation-backend-py
# Или с отдельным compose файлом
cd moderation/backend-py
docker-compose -f docker-compose.moderation-py.yml up -d
```
## Nginx интеграция
Если используете Nginx как reverse proxy, добавьте:
```nginx
upstream moderation_backend_py {
server 127.0.0.1:3001;
}
server {
listen 443 ssl http2;
server_name moderation.nkm.guru;
# SSL certificates
ssl_certificate /etc/letsencrypt/live/moderation.nkm.guru/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/moderation.nkm.guru/privkey.pem;
# API
location /api/ {
proxy_pass http://moderation_backend_py;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# WebSocket
location /socket.io/ {
proxy_pass http://moderation_backend_py;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
```
Перезагрузите Nginx:
```bash
sudo nginx -t
sudo systemctl reload nginx
```
## Troubleshooting
### Контейнер не запускается
```bash
# Проверить логи
docker logs nakama-moderation-py
# Проверить .env
ls -la ../../.env
# Проверить MongoDB доступность
docker run --rm mongo:7 mongosh "mongodb://103.80.87.247:27017/nakama" --eval "db.adminCommand('ping')"
```
### Email не работает
```bash
# Войти в контейнер и проверить
docker exec -it nakama-moderation-py python test_email.py
```
### Порт занят
```bash
# Проверить, что запущено на порту 3001
sudo lsof -i :3001
# или
sudo netstat -tulpn | grep 3001
# Остановить Node.js версию
pm2 stop moderation-backend
```
## Мониторинг
```bash
# Статус контейнера
docker ps | grep moderation-py
# Использование ресурсов
docker stats nakama-moderation-py
# Логи с меткой времени
docker logs -f --timestamps nakama-moderation-py
```
---
**Готово! Запускайте через `./docker-start.sh`** 🚀

View File

@ -0,0 +1,109 @@
═══════════════════════════════════════════════════════════════
🐳 DOCKER ЗАПУСК - Python Moderation Backend
═══════════════════════════════════════════════════════════════
📋 ШАГ 1: Настройте .env
───────────────────────────────────────────────────────────────
Откройте nakama/.env и добавьте:
EMAIL_PROVIDER=yandex
YANDEX_SMTP_USER=ваш_email@yandex.ru
YANDEX_SMTP_PASSWORD=ваш_пароль_приложения
YANDEX_SMTP_HOST=smtp.yandex.ru
YANDEX_SMTP_PORT=465
EMAIL_FROM=noreply@nakama.guru
🔑 Пароль приложения: https://id.yandex.ru/security
📋 ШАГ 2: Создайте админа в MongoDB
───────────────────────────────────────────────────────────────
mongosh nakama
db.users.updateOne(
{ username: "glpshchn00" },
{ $set: { email: "aaem9848@gmail.com", role: "admin", emailVerified: true } }
);
📋 ШАГ 3: Запустите Docker
───────────────────────────────────────────────────────────────
cd moderation/backend-py
./docker-start.sh
ИЛИ вручную:
docker build -t nakama-moderation-py .
docker run -d --name nakama-moderation-py -p 3001:3001 --env-file ../../.env nakama-moderation-py
📋 ШАГ 4: Проверьте
───────────────────────────────────────────────────────────────
curl http://localhost:3001/health
# Должен вернуть:
# {"status":"ok","service":"moderation","version":"2.0.0-python"}
# Логи:
docker logs -f nakama-moderation-py
═══════════════════════════════════════════════════════════════
📝 ПОЛЕЗНЫЕ КОМАНДЫ
═══════════════════════════════════════════════════════════════
# Логи
docker logs nakama-moderation-py
docker logs -f nakama-moderation-py # real-time
# Управление
docker stop nakama-moderation-py
docker start nakama-moderation-py
docker restart nakama-moderation-py
docker rm -f nakama-moderation-py
# Зайти в контейнер
docker exec -it nakama-moderation-py bash
# Проверить статус
docker ps | grep moderation-py
# Пересобрать и перезапустить
docker build -t nakama-moderation-py . && \
docker stop nakama-moderation-py && \
docker rm nakama-moderation-py && \
./docker-start.sh
═══════════════════════════════════════════════════════════════
🔧 TROUBLESHOOTING
═══════════════════════════════════════════════════════════════
❌ Контейнер не запускается:
docker logs nakama-moderation-py
❌ Email не работает:
docker exec -it nakama-moderation-py python test_email.py
❌ Порт 3001 занят:
sudo lsof -i :3001
pm2 stop moderation-backend # остановить Node.js версию
❌ MongoDB недоступна:
docker run --rm mongo:7 mongosh "mongodb://ваш_uri" --eval "db.adminCommand('ping')"
═══════════════════════════════════════════════════════════════
📚 ДОКУМЕНТАЦИЯ
═══════════════════════════════════════════════════════════════
START_HERE.md - 👈 Начните здесь
DOCKER.md - Подробно про Docker
QUICKSTART.md - Быстрый старт
MIGRATION.md - Миграция с Node.js
═══════════════════════════════════════════════════════════════
Готово! 🎉
═══════════════════════════════════════════════════════════════

View File

@ -0,0 +1,28 @@
FROM python:3.11-slim
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y \
gcc \
&& rm -rf /var/lib/apt/lists/*
# Copy requirements
COPY requirements.txt .
# Install Python dependencies
RUN pip install --no-cache-dir -r requirements.txt
# Copy application
COPY . .
# Expose port
EXPOSE 3001
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:3001/health')"
# Run application
CMD ["python", "-m", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "3001"]

View File

@ -0,0 +1,165 @@
# Установка и запуск Python Moderation Backend
## Быстрая установка
```bash
cd moderation/backend-py
./start.sh
```
Скрипт автоматически:
1. Создаст виртуальное окружение
2. Установит зависимости
3. Запустит сервер
## Ручная установка
### 1. Python 3.11+
```bash
python3 --version
```
Если версия < 3.11, установите новую версию.
### 2. Виртуальное окружение
```bash
cd moderation/backend-py
python3 -m venv venv
source venv/bin/activate
```
### 3. Зависимости
```bash
pip install -r requirements.txt
```
### 4. Настройка email
В корневом `.env` (`nakama/.env`) добавьте:
```env
EMAIL_PROVIDER=yandex
YANDEX_SMTP_HOST=smtp.yandex.ru
YANDEX_SMTP_PORT=465
YANDEX_SMTP_SECURE=true
YANDEX_SMTP_USER=ваш_email@yandex.ru
YANDEX_SMTP_PASSWORD=ваш_пароль_приложения
EMAIL_FROM=noreply@nakama.guru
```
**Получение пароля приложения Yandex:**
1. https://id.yandex.ru/security
2. "Пароли и вход" → "Пароли приложений"
3. Создать для "Почта"
4. Скопировать в `YANDEX_SMTP_PASSWORD`
### 5. Создайте админа
```javascript
// В mongosh
use nakama;
// Найти вашего пользователя
const user = db.users.findOne({ username: "ваш_username" });
// Обновить email и роль
db.users.updateOne(
{ _id: user._id },
{
$set: {
email: "ваш_email@yandex.ru",
emailVerified: true,
role: "admin"
}
}
);
// Проверить
db.users.findOne({ email: "ваш_email@yandex.ru" });
```
### 6. Запуск
```bash
python main.py
```
Или:
```bash
uvicorn main:app --reload --host 0.0.0.0 --port 3001
```
### 7. Проверка
```bash
# Health check
curl http://localhost:3001/health
# Должен вернуть:
# {"status":"ok","service":"moderation","version":"2.0.0-python"}
```
## Тестирование email
```bash
# В Python консоли
python3
```
```python
import asyncio
from utils.email_service import send_verification_code
async def test():
await send_verification_code("ваш_email@yandex.ru", "123456")
print("✅ Email отправлен!")
asyncio.run(test())
```
Если ошибка - проверьте настройки SMTP в `.env`.
## Переключение с Node.js
1. Остановите Node.js версию:
```bash
pm2 stop moderation-backend
```
2. Запустите Python версию:
```bash
cd moderation/backend-py
./start.sh
```
3. Фронтенд работает без изменений!
## Production
```bash
# С PM2
pm2 start "uvicorn main:app --host 0.0.0.0 --port 3001 --workers 2" \
--name moderation-backend-py \
--cwd /path/to/nakama/moderation/backend-py \
--interpreter python3
# Или с Docker
docker-compose -f docker-compose.moderation-py.yml up -d
```
## Логи
Все логи в stdout:
- `[Email]` - email операции
- `[ModerationAuth]` - аутентификация
- `[ModApp]` - модерация
- `[WebSocket]` - WebSocket
## Помощь
См. `README.md` и `MIGRATION.md` для подробностей.

View File

@ -0,0 +1,141 @@
# Миграция с Node.js на Python
## Зачем переходить на Python?
1. **Email работает из коробки** - нативная поддержка SMTP без проблем с AWS SDK
2. **Проще настройка** - меньше зависимостей и конфликтов версий
3. **Лучшая типизация** - Pydantic для валидации
4. **Совместимость** - использует ту же MongoDB, API полностью совместимо
## Шаги миграции
### 1. Остановите Node.js версию
```bash
# Если запущено через PM2
pm2 stop moderation-backend
# Если запущено через Docker
docker stop nakama-moderation-backend
# Если запущено вручную
# Найдите процесс и остановите его
ps aux | grep "moderation/backend"
kill <PID>
```
### 2. Установите Python зависимости
```bash
cd moderation/backend-py
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
```
### 3. Проверьте .env
Убедитесь, что в корневом `.env` (`nakama/.env`) настроены:
```env
# Email (обязательно!)
EMAIL_PROVIDER=yandex
YANDEX_SMTP_HOST=smtp.yandex.ru
YANDEX_SMTP_PORT=465
YANDEX_SMTP_SECURE=true
YANDEX_SMTP_USER=ваш_email@yandex.ru
YANDEX_SMTP_PASSWORD=ваш_пароль_приложения
EMAIL_FROM=noreply@nakama.guru
OWNER_EMAIL=admin@example.com
# Модерация
MODERATION_PORT=3001
MODERATION_BOT_TOKEN=ваш_токен
MODERATION_OWNER_USERNAMES=glpshchn00
# MongoDB (та же база!)
MONGODB_URI=mongodb://103.80.87.247:27017/nakama
```
### 4. Запустите Python версию
```bash
# Development
python main.py
# Или с auto-reload
uvicorn main:app --reload --host 0.0.0.0 --port 3001
# Production
uvicorn main:app --host 0.0.0.0 --port 3001 --workers 4
```
### 5. Проверьте работу
```bash
# Health check
curl http://localhost:3001/health
# API
curl http://localhost:3001/api/moderation-auth/config
```
### 6. Обновите PM2 (опционально)
```bash
pm2 delete moderation-backend
pm2 start "uvicorn main:app --host 0.0.0.0 --port 3001 --workers 2" \
--name moderation-backend-py \
--cwd /path/to/nakama/moderation/backend-py \
--interpreter python3
```
## Откат на Node.js
Если нужно вернуться:
```bash
# Остановите Python версию
pm2 stop moderation-backend-py
# Запустите Node.js версию
cd moderation/backend
npm start
```
## Проверка совместимости
Фронтенд модерации работает с обеими версиями без изменений:
- API endpoints идентичны
- Формат данных совместим
- WebSocket протокол тот же
## Решение проблем
### Email не отправляется
Проверьте:
1. Используете ли пароль приложения Yandex (не основной пароль)
2. Правильность email адреса в `YANDEX_SMTP_USER`
3. Логи при запуске - должна быть строка `[Email] Настройка SMTP`
### 401 Unauthorized
- JWT токены те же, что и в Node.js версии
- Cookies с теми же именами
- Проверьте `JWT_ACCESS_SECRET` в `.env`
### MongoDB connection error
- Используется та же `MONGODB_URI` из `.env`
- Проверьте доступность MongoDB: `mongosh <MONGODB_URI>`
## Производительность
Python версия с uvicorn и 4 workers показывает сопоставимую или лучшую производительность по сравнению с Node.js.
## Поддержка
При проблемах создайте issue или обратитесь к администратору.

View File

@ -0,0 +1,118 @@
# Быстрый старт - Python Moderation Backend
## 1. Установка
```bash
cd moderation/backend-py
# Создать виртуальное окружение
python3 -m venv venv
# Активировать
source venv/bin/activate # Linux/Mac
# или
venv\Scripts\activate # Windows
# Установить зависимости
pip install -r requirements.txt
```
## 2. Настройка .env
Отредактируйте корневой `.env` файл (`nakama/.env`):
```env
# Email (ОБЯЗАТЕЛЬНО!)
EMAIL_PROVIDER=yandex
YANDEX_SMTP_HOST=smtp.yandex.ru
YANDEX_SMTP_PORT=465
YANDEX_SMTP_SECURE=true
YANDEX_SMTP_USER=ваш_email@yandex.ru
YANDEX_SMTP_PASSWORD=ваш_пароль_приложения
EMAIL_FROM=noreply@nakama.guru
OWNER_EMAIL=admin@example.com
# Модерация
MODERATION_PORT=3001
MODERATION_BOT_TOKEN=ваш_токен
# MongoDB
MONGODB_URI=mongodb://103.80.87.247:27017/nakama
```
**Важно:** Для Yandex используйте пароль приложения!
1. Перейдите: https://id.yandex.ru/security
2. Создайте пароль приложения для "Почта"
3. Используйте его в `YANDEX_SMTP_PASSWORD`
## 3. Создайте админа в MongoDB
```javascript
// В mongosh
use nakama;
db.users.updateOne(
{ username: "ваш_username" },
{
$set: {
email: "ваш_email@yandex.ru",
emailVerified: true,
role: "admin"
}
}
);
```
## 4. Запуск
```bash
# Простой запуск
python main.py
# Или с auto-reload
uvicorn main:app --reload --host 0.0.0.0 --port 3001
# Или через скрипт
./start.sh
```
## 5. Проверка
```bash
# Health check
curl http://localhost:3001/health
# Config
curl http://localhost:3001/api/moderation-auth/config
```
## 6. Отправка кода
Откройте фронтенд модерации и введите email. Код должен прийти на почту.
## Troubleshooting
### Email не отправляется
Проверьте логи при запуске:
```
[Email] Настройка SMTP: {provider: 'yandex', host: 'smtp.yandex.ru', ...}
```
Если видите `user: 'не указан'` или `hasPassword: False` - проверьте `.env`.
### 403 Forbidden при send-code
Убедитесь, что в базе есть пользователь с:
- email = тот, что вводите
- role = 'admin' или 'moderator'
### Telegram виджет не работает
После успешной авторизации должно быть автоматическое перенаправление.
Проверьте логи: `[ModerationAuth] Успешная авторизация через виджет`
## Production
См. `MIGRATION.md` для полных инструкций по развертыванию.

View File

@ -0,0 +1,208 @@
# Nakama Moderation Backend - Python
Бэкенд модерации на Python с FastAPI, портированный с Node.js.
## Преимущества Python версии
- ✅ **Лучшая работа с email** - нативная поддержка SMTP без проблем с AWS SDK
- ✅ **Простота настройки** - меньше зависимостей и конфликтов
- ✅ **Производительность** - async/await с uvicorn
- ✅ **Типизация** - Pydantic для валидации данных
- ✅ **Совместимость** - использует ту же MongoDB и те же данные
## Установка
### 1. Установите Python 3.11+
```bash
python3 --version # Должно быть >= 3.11
```
### 2. Создайте виртуальное окружение
```bash
cd moderation/backend-py
python3 -m venv venv
source venv/bin/activate # Linux/Mac
# или
venv\Scripts\activate # Windows
```
### 3. Установите зависимости
```bash
pip install -r requirements.txt
```
### 4. Настройте .env
Используется корневой `.env` файл проекта (в `nakama/.env`).
Обязательные переменные для email:
```env
EMAIL_PROVIDER=yandex
YANDEX_SMTP_HOST=smtp.yandex.ru
YANDEX_SMTP_PORT=465
YANDEX_SMTP_SECURE=true
YANDEX_SMTP_USER=ваш_email@yandex.ru
YANDEX_SMTP_PASSWORD=ваш_пароль_приложения
EMAIL_FROM=noreply@nakama.guru
OWNER_EMAIL=admin@example.com
```
**Важно:** Для Yandex используйте пароль приложения (https://id.yandex.ru/security), не основной пароль!
## Запуск
### Development режим
```bash
cd moderation/backend-py
source venv/bin/activate
python main.py
```
Или через uvicorn с auto-reload:
```bash
uvicorn main:app --reload --host 0.0.0.0 --port 3001
```
### Production режим
```bash
uvicorn main:app --host 0.0.0.0 --port 3001 --workers 4
```
### Docker
```bash
# Сборка
docker build -t nakama-moderation-py -f moderation/backend-py/Dockerfile moderation/backend-py
# Запуск
docker run -d \
--name nakama-moderation-py \
-p 3001:3001 \
--env-file ../../.env \
nakama-moderation-py
```
## API Endpoints
### Authentication (`/api/moderation-auth`)
- `POST /send-code` - Отправить код на email
- `POST /register` - Регистрация с кодом
- `POST /login` - Вход по email/паролю
- `POST /telegram-widget` - Вход через Telegram виджет
- `POST /logout` - Выход
- `GET /config` - Получить конфигурацию
- `GET /me` - Текущий пользователь
### Moderation (`/api/mod-app`)
- `GET /users` - Список пользователей
- `PUT /users/{id}/ban` - Забанить пользователя
- `GET /posts` - Список постов
- `GET /posts/{id}` - Получить пост
- `PUT /posts/{id}` - Обновить пост
- `DELETE /posts/{id}` - Удалить пост
- `GET /reports` - Список репортов
- `PUT /reports/{id}` - Обновить статус репорта
- `GET /admins` - Список админов
- `POST /auth/verify` - Верификация авторизации
### WebSocket (`/socket.io`)
- `join_moderation_chat` - Присоединиться к чату
- `leave_moderation_chat` - Покинуть чат
- `moderation_message` - Отправить сообщение
- `typing` - Индикатор печати
## Структура проекта
```
moderation/backend-py/
├── main.py # Главный файл приложения
├── config.py # Конфигурация
├── database.py # MongoDB подключение
├── models.py # Pydantic модели
├── middleware.py # Middleware (logging, security)
├── websocket_server.py # WebSocket сервер
├── requirements.txt # Python зависимости
├── Dockerfile # Docker образ
├── routes/
│ ├── __init__.py
│ ├── mod_app.py # Роуты модерации
│ └── moderation_auth.py # Роуты аутентификации
└── utils/
├── __init__.py
├── auth.py # JWT и авторизация
├── email_service.py # Отправка email
└── minio_client.py # MinIO клиент
```
## Миграция с Node.js
Для переключения с Node.js версии на Python:
1. Остановите Node.js бэкенд модерации
2. Запустите Python версию на том же порту (3001)
3. Фронтенд будет работать без изменений (API совместимо)
## Отладка
### Проверка email настроек
```python
python -c "from config import settings; print(f'Email: {settings.EMAIL_PROVIDER}, User: {settings.YANDEX_SMTP_USER}')"
```
### Проверка MongoDB подключения
```python
python -c "import asyncio; from database import connect_db; asyncio.run(connect_db())"
```
### Логи
Все логи выводятся в stdout с префиксами:
- `[Email]` - email операции
- `[ModerationAuth]` - аутентификация
- `[ModApp]` - модерация
- `[WebSocket]` - WebSocket события
## Troubleshooting
### Email не отправляется
1. Проверьте переменные в `.env`:
```bash
grep YANDEX_SMTP ../../.env
```
2. Убедитесь, что используете пароль приложения Yandex
3. Проверьте логи при запуске - должно быть:
```
[Email] Настройка SMTP: {provider: 'yandex', host: 'smtp.yandex.ru', ...}
```
### 401 Unauthorized
Проверьте JWT токены в cookies или Authorization header.
### WebSocket не подключается
Убедитесь, что фронтенд подключается к правильному URL:
- Development: `http://localhost:3001`
- Production: ваш домен с правильным портом
## Production Deployment
См. `moderation/DEPLOY.md` для инструкций по развертыванию.

View File

@ -0,0 +1,44 @@
#!/bin/bash
echo ""
echo "═══════════════════════════════════════════════════════════════"
echo "🐍 NAKAMA MODERATION BACKEND - PYTHON"
echo "═══════════════════════════════════════════════════════════════"
echo ""
# Check if running from correct directory
if [ ! -f "main.py" ]; then
echo "❌ Запустите скрипт из директории moderation/backend-py!"
exit 1
fi
echo "Выберите способ запуска:"
echo ""
echo " 1) 🐳 Docker (рекомендуется)"
echo " 2) 🐍 Python venv (локально)"
echo " 3) 📋 Показать инструкции"
echo ""
read -p "Введите номер (1-3): " choice
case $choice in
1)
echo ""
echo "🐳 Запуск через Docker..."
./docker-start.sh
;;
2)
echo ""
echo "🐍 Запуск через Python venv..."
./start.sh
;;
3)
echo ""
cat DOCKER_QUICK.txt
;;
*)
echo ""
echo "❌ Неверный выбор"
exit 1
;;
esac

View File

@ -0,0 +1,122 @@
# 🎯 НАЧНИТЕ ЗДЕСЬ - Python Moderation Backend
## Что это?
Полностью рабочий бэкенд модерации на Python3 с FastAPI.
Решает все проблемы с email и авторизацией из Node.js версии.
## Быстрый запуск (3 шага)
### Шаг 1: Настройте email в .env
Откройте `nakama/.env` (корневой файл) и добавьте:
```env
EMAIL_PROVIDER=yandex
YANDEX_SMTP_HOST=smtp.yandex.ru
YANDEX_SMTP_PORT=465
YANDEX_SMTP_SECURE=true
YANDEX_SMTP_USER=ваш_email@yandex.ru
YANDEX_SMTP_PASSWORD=ваш_пароль_приложения
EMAIL_FROM=noreply@nakama.guru
```
**Где взять пароль приложения:**
1. https://id.yandex.ru/security
2. "Пароли приложений" → Создать для "Почта"
3. Скопировать в `YANDEX_SMTP_PASSWORD`
### Шаг 2: Создайте админа
```bash
mongosh nakama
```
```javascript
db.users.updateOne(
{ username: "glpshchn00" }, // ваш username
{
$set: {
email: "aaem9848@gmail.com", // ваш email
emailVerified: true,
role: "admin"
}
}
);
```
### Шаг 3: Запустите сервер
```bash
cd moderation/backend-py
./start.sh
```
Готово! Сервер запущен на http://localhost:3001
## Проверка
```bash
# Health check
curl http://localhost:3001/health
# Должен вернуть:
# {"status":"ok","service":"moderation","version":"2.0.0-python"}
```
Откройте фронтенд модерации и попробуйте отправить код на email - должно работать!
## Что дальше?
- `QUICKSTART.md` - быстрый старт с примерами
- `INSTALL.md` - подробная установка
- `README.md` - полная документация
- `MIGRATION.md` - миграция с Node.js
## Проблемы?
### Email не отправляется
1. Проверьте, что используете **пароль приложения**, не основной пароль Yandex
2. Проверьте логи - должна быть строка: `[Email] Настройка SMTP`
3. Убедитесь, что переменные в `.env` без кавычек и пробелов
### 403 Forbidden
Проверьте роль пользователя:
```javascript
db.users.findOne({ email: "ваш_email@yandex.ru" })
// role должна быть "admin" или "moderator"
```
### Сервер не запускается
```bash
# Проверьте Python версию (нужна 3.11+)
python3 --version
# Переустановите зависимости
pip install -r requirements.txt
```
## Переключение с Node.js
Если у вас уже запущена Node.js версия:
```bash
# Остановите Node.js
pm2 stop moderation-backend
# Запустите Python
cd moderation/backend-py
./start.sh
```
Фронтенд продолжит работать без изменений!
---
**Готово к использованию! 🎉**
Email работает, авторизация работает, API совместимо с фронтендом.

View File

@ -0,0 +1,108 @@
"""
Configuration management for moderation backend
"""
import os
from pathlib import Path
from pydantic_settings import BaseSettings
from dotenv import load_dotenv
# Load .env from project root
root_env_path = Path(__file__).parent.parent.parent / '.env'
if root_env_path.exists():
load_dotenv(dotenv_path=root_env_path)
print(f"✅ Loaded .env from: {root_env_path}")
else:
print(f"⚠️ .env file not found at: {root_env_path}")
class Settings(BaseSettings):
"""Application settings"""
# Server
MODERATION_PORT: int = int(os.getenv('MODERATION_PORT', '3001'))
NODE_ENV: str = os.getenv('NODE_ENV', 'development')
# MongoDB
MONGODB_URI: str = os.getenv('MONGODB_URI', 'mongodb://localhost:27017/nakama')
# JWT
JWT_ACCESS_SECRET: str = os.getenv('JWT_ACCESS_SECRET', 'nakama_access_secret_change_me')
JWT_REFRESH_SECRET: str = os.getenv('JWT_REFRESH_SECRET', 'nakama_refresh_secret_change_me')
JWT_ACCESS_EXPIRES_IN: int = int(os.getenv('JWT_ACCESS_EXPIRES_IN', '300'))
JWT_REFRESH_EXPIRES_IN: int = int(os.getenv('JWT_REFRESH_EXPIRES_IN', '604800'))
JWT_ACCESS_COOKIE_NAME: str = os.getenv('JWT_ACCESS_COOKIE_NAME', 'nakama_access_token')
JWT_REFRESH_COOKIE_NAME: str = os.getenv('JWT_REFRESH_COOKIE_NAME', 'nakama_refresh_token')
# Telegram
TELEGRAM_BOT_TOKEN: str = os.getenv('TELEGRAM_BOT_TOKEN', '')
MODERATION_BOT_TOKEN: str = os.getenv('MODERATION_BOT_TOKEN', '')
MODERATION_BOT_USERNAME: str = os.getenv('MODERATION_BOT_USERNAME', '')
MODERATION_OWNER_USERNAMES: str = os.getenv('MODERATION_OWNER_USERNAMES', 'glpshchn00')
MODERATION_CHANNEL_USERNAME: str = os.getenv('MODERATION_CHANNEL_USERNAME', '@reichenbfurry')
# Frontend
FRONTEND_URL: str = os.getenv('FRONTEND_URL', 'http://localhost:5173')
MODERATION_CORS_ORIGIN: str = os.getenv('MODERATION_CORS_ORIGIN', '*')
# Email
EMAIL_PROVIDER: str = os.getenv('EMAIL_PROVIDER', 'yandex')
EMAIL_FROM: str = os.getenv('EMAIL_FROM', 'noreply@nakama.guru')
OWNER_EMAIL: str = os.getenv('OWNER_EMAIL', 'aaem9848@gmail.com')
# AWS SES / Yandex Cloud Postbox
AWS_SES_ACCESS_KEY_ID: str = os.getenv('AWS_SES_ACCESS_KEY_ID', '')
AWS_SES_SECRET_ACCESS_KEY: str = os.getenv('AWS_SES_SECRET_ACCESS_KEY', '')
AWS_SES_REGION: str = os.getenv('AWS_SES_REGION', 'us-east-1')
AWS_SES_ENDPOINT_URL: str = os.getenv('AWS_SES_ENDPOINT_URL', '')
# Yandex SMTP
YANDEX_SMTP_HOST: str = os.getenv('YANDEX_SMTP_HOST', 'smtp.yandex.ru')
YANDEX_SMTP_PORT: int = int(os.getenv('YANDEX_SMTP_PORT', '465'))
YANDEX_SMTP_USER: str = os.getenv('YANDEX_SMTP_USER', '')
YANDEX_SMTP_PASSWORD: str = os.getenv('YANDEX_SMTP_PASSWORD', '')
YANDEX_SMTP_SECURE: bool = os.getenv('YANDEX_SMTP_SECURE', 'true').lower() == 'true'
# MinIO
MINIO_ENABLED: bool = os.getenv('MINIO_ENABLED', 'false').lower() == 'true'
MINIO_ENDPOINT: str = os.getenv('MINIO_ENDPOINT', 'localhost')
MINIO_PORT: int = int(os.getenv('MINIO_PORT', '9000'))
MINIO_USE_SSL: bool = os.getenv('MINIO_USE_SSL', 'false').lower() == 'true'
MINIO_ACCESS_KEY: str = os.getenv('MINIO_ACCESS_KEY', 'minioadmin')
MINIO_SECRET_KEY: str = os.getenv('MINIO_SECRET_KEY', 'minioadmin')
MINIO_BUCKET: str = os.getenv('MINIO_BUCKET', 'nakama-media')
@property
def IS_DEVELOPMENT(self) -> bool:
return self.NODE_ENV == 'development'
@property
def IS_PRODUCTION(self) -> bool:
return self.NODE_ENV == 'production'
@property
def OWNER_USERNAMES_LIST(self) -> list[str]:
return [u.strip().lower() for u in self.MODERATION_OWNER_USERNAMES.split(',') if u.strip()]
class Config:
case_sensitive = True
settings = Settings()
# Logging config
def print_config():
"""Print configuration on startup"""
print("\n📋 Конфигурация:")
print(f" Environment: {settings.NODE_ENV}")
print(f" Port: {settings.MODERATION_PORT}")
print(f" MongoDB: {settings.MONGODB_URI.split('@')[-1] if '@' in settings.MONGODB_URI else settings.MONGODB_URI}")
print(f" Email Provider: {settings.EMAIL_PROVIDER}")
print(f" MinIO: {'Enabled' if settings.MINIO_ENABLED else 'Disabled'}")
print(f" CORS Origin: {settings.MODERATION_CORS_ORIGIN}")
print()
if __name__ == "__main__":
print_config()

View File

@ -0,0 +1,94 @@
"""
MongoDB connection and database utilities
"""
from motor.motor_asyncio import AsyncIOMotorClient
from pymongo.errors import ConnectionFailure
from config import settings
# Global MongoDB client
mongodb_client: AsyncIOMotorClient = None
db = None
async def connect_db():
"""Connect to MongoDB"""
global mongodb_client, db
try:
print(f"🔌 Подключение к MongoDB: {settings.MONGODB_URI.split('@')[-1] if '@' in settings.MONGODB_URI else settings.MONGODB_URI}")
mongodb_client = AsyncIOMotorClient(
settings.MONGODB_URI,
maxPoolSize=10,
minPoolSize=1,
serverSelectionTimeoutMS=5000
)
# Test connection
await mongodb_client.admin.command('ping')
# Get database name from URI or use default
db_name = settings.MONGODB_URI.split('/')[-1].split('?')[0] or 'nakama'
db = mongodb_client[db_name]
print(f"✅ MongoDB подключена (база: {db_name})")
# Print collections count
collections = await db.list_collection_names()
print(f" 📚 Коллекций в базе: {len(collections)}")
except ConnectionFailure as e:
print(f"Не удалось подключиться к MongoDB: {e}")
raise
except Exception as e:
print(f"❌ Ошибка подключения к MongoDB: {e}")
raise
async def close_db():
"""Close MongoDB connection"""
global mongodb_client
if mongodb_client:
mongodb_client.close()
print("✅ MongoDB отключена")
def get_db():
"""Get database instance"""
return db
def get_collection(collection_name: str):
"""Get collection by name"""
return db[collection_name]
# Collection shortcuts
def users_collection():
return get_collection('users')
def posts_collection():
return get_collection('posts')
def reports_collection():
return get_collection('reports')
def moderation_admins_collection():
return get_collection('moderationadmins')
def email_verification_codes_collection():
return get_collection('emailverificationcodes')
def admin_confirmations_collection():
return get_collection('adminconfirmations')
def notifications_collection():
return get_collection('notifications')

View File

@ -0,0 +1,50 @@
version: '3.8'
services:
moderation-backend-py:
build:
context: .
dockerfile: Dockerfile
container_name: nakama-moderation-backend-py
restart: unless-stopped
ports:
- "127.0.0.1:3001:3001"
environment:
- NODE_ENV=production
- MODERATION_PORT=3001
- MONGODB_URI=${MONGODB_URI}
- JWT_ACCESS_SECRET=${JWT_ACCESS_SECRET}
- JWT_REFRESH_SECRET=${JWT_REFRESH_SECRET}
- MODERATION_BOT_TOKEN=${MODERATION_BOT_TOKEN}
- MODERATION_OWNER_USERNAMES=${MODERATION_OWNER_USERNAMES}
- MODERATION_CORS_ORIGIN=${MODERATION_CORS_ORIGIN}
# Email
- EMAIL_PROVIDER=${EMAIL_PROVIDER}
- EMAIL_FROM=${EMAIL_FROM}
- OWNER_EMAIL=${OWNER_EMAIL}
- YANDEX_SMTP_HOST=${YANDEX_SMTP_HOST}
- YANDEX_SMTP_PORT=${YANDEX_SMTP_PORT}
- YANDEX_SMTP_USER=${YANDEX_SMTP_USER}
- YANDEX_SMTP_PASSWORD=${YANDEX_SMTP_PASSWORD}
- YANDEX_SMTP_SECURE=${YANDEX_SMTP_SECURE}
# MinIO
- MINIO_ENABLED=${MINIO_ENABLED}
- MINIO_ENDPOINT=${MINIO_ENDPOINT}
- MINIO_PORT=${MINIO_PORT}
- MINIO_USE_SSL=${MINIO_USE_SSL}
- MINIO_ACCESS_KEY=${MINIO_ACCESS_KEY}
- MINIO_SECRET_KEY=${MINIO_SECRET_KEY}
- MINIO_BUCKET=${MINIO_BUCKET}
networks:
- nakama-network
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:3001/health')"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
networks:
nakama-network:
external: true

View File

@ -0,0 +1,59 @@
#!/bin/bash
# Docker start script for Python Moderation Backend
echo "🐳 Запуск Nakama Moderation Backend (Python) через Docker..."
# Check if .env exists
if [ ! -f "../../.env" ]; then
echo "❌ Файл .env не найден в nakama/.env!"
exit 1
fi
# Build image
echo "📦 Сборка Docker образа..."
docker build -t nakama-moderation-py .
if [ $? -ne 0 ]; then
echo "❌ Ошибка сборки образа"
exit 1
fi
# Stop old container if exists
if [ "$(docker ps -aq -f name=nakama-moderation-py)" ]; then
echo "🛑 Остановка старого контейнера..."
docker stop nakama-moderation-py
docker rm nakama-moderation-py
fi
# Run container
echo "🚀 Запуск контейнера..."
docker run -d \
--name nakama-moderation-py \
-p 3001:3001 \
--env-file ../../.env \
--restart unless-stopped \
nakama-moderation-py
if [ $? -ne 0 ]; then
echo "❌ Ошибка запуска контейнера"
exit 1
fi
echo ""
echo "✅ Контейнер запущен!"
echo ""
echo "📝 Полезные команды:"
echo " docker logs nakama-moderation-py # Логи"
echo " docker logs -f nakama-moderation-py # Логи в реальном времени"
echo " docker stop nakama-moderation-py # Остановить"
echo " docker restart nakama-moderation-py # Перезапустить"
echo ""
echo "🌐 API: http://localhost:3001/api"
echo "🔍 Health: curl http://localhost:3001/health"
echo ""
# Show logs
echo "📋 Последние логи:"
docker logs --tail 20 nakama-moderation-py

View File

@ -0,0 +1,101 @@
"""
Nakama Moderation Backend - Python FastAPI
"""
import os
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.trustedhost import TrustedHostMiddleware
import uvicorn
from config import settings
from database import connect_db, close_db
from middleware import setup_middleware
from routes.mod_app import router as mod_app_router
from routes.moderation_auth import router as moderation_auth_router
from websocket_server import get_socketio_app
from utils.minio_client import init_minio
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Lifecycle management"""
# Startup
print("\n" + "=" * 60)
print("🚀 Запуск сервера модерации (Python)...")
await connect_db()
# Initialize MinIO
if settings.MINIO_ENABLED:
init_minio()
print("=" * 60 + "\n")
yield
# Shutdown
print("\n" + "=" * 60)
print("🛑 Остановка сервера модерации...")
await close_db()
print("=" * 60 + "\n")
app = FastAPI(
title="Nakama Moderation API",
description="API для модерации Nakama",
version="2.0.0",
lifespan=lifespan
)
# CORS
app.add_middleware(
CORSMiddleware,
allow_origins=settings.MODERATION_CORS_ORIGIN.split(',') if settings.MODERATION_CORS_ORIGIN != '*' else ['*'],
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allow_headers=["Content-Type", "Authorization", "x-telegram-init-data"],
max_age=86400,
)
# Setup middleware (security, logging, etc.)
setup_middleware(app)
# Health check
@app.get("/health")
async def health_check():
return {
"status": "ok",
"service": "moderation",
"version": "2.0.0-python"
}
# Root endpoint
@app.get("/")
async def root():
return {"message": "Nakama Moderation API - Python"}
# Include routers
app.include_router(mod_app_router, prefix="/api/mod-app", tags=["moderation"])
app.include_router(moderation_auth_router, prefix="/api/moderation-auth", tags=["auth"])
# Mount Socket.IO app for WebSocket
socketio_app = get_socketio_app()
app.mount("/socket.io", socketio_app)
if __name__ == "__main__":
print("\n" + "=" * 60)
print("✅ Сервер модерации запущен (Python)")
print(f" 🌐 API: http://0.0.0.0:{settings.MODERATION_PORT}/api")
print(f" 🔌 WebSocket: http://0.0.0.0:{settings.MODERATION_PORT}/socket.io")
print(f" 📦 MongoDB: {settings.MONGODB_URI.split('@')[-1] if '@' in settings.MONGODB_URI else settings.MONGODB_URI}")
print("=" * 60 + "\n")
uvicorn.run(
"main:app",
host="0.0.0.0",
port=settings.MODERATION_PORT,
reload=settings.IS_DEVELOPMENT,
log_level="info"
)

View File

@ -0,0 +1,73 @@
"""
Middleware for FastAPI application
"""
import time
import logging
from typing import Callable
from fastapi import Request, Response
from fastapi.responses import JSONResponse
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded
# Setup logging
logging.basicConfig(
level=logging.INFO,
format='[%(asctime)s] %(levelname)s: %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
logger = logging.getLogger(__name__)
# Rate limiter
limiter = Limiter(key_func=get_remote_address)
async def log_requests(request: Request, call_next: Callable):
"""Log all incoming requests"""
start_time = time.time()
# Log request
logger.info(f"🔍 [DEBUG] Incoming request {{method: '{request.method}', path: '{request.url.path}', ip: '{request.client.host}'}}")
# Process request
response = await call_next(request)
# Calculate duration
duration = int((time.time() - start_time) * 1000)
# Log response
status_emoji = "📝" if 200 <= response.status_code < 300 else "⚠️" if 400 <= response.status_code < 500 else ""
level = "INFO" if 200 <= response.status_code < 300 else "WARN" if 400 <= response.status_code < 500 else "ERROR"
logger.log(
getattr(logging, level),
f"{status_emoji} [{level}] Request completed {{method: '{request.method}', path: '{request.url.path}', status: {response.status_code}, duration: '{duration}ms'}}"
)
return response
async def security_headers(request: Request, call_next: Callable):
"""Add security headers"""
response = await call_next(request)
response.headers['X-Content-Type-Options'] = 'nosniff'
response.headers['X-Frame-Options'] = 'DENY'
response.headers['X-XSS-Protection'] = '1; mode=block'
response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains'
return response
def setup_middleware(app):
"""Setup all middleware"""
# Request logging
app.middleware("http")(log_requests)
# Security headers
app.middleware("http")(security_headers)
# Rate limiting
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)

View File

@ -0,0 +1,157 @@
"""
Pydantic models for request/response validation
"""
from datetime import datetime
from typing import Optional, List
from pydantic import BaseModel, EmailStr, Field
from bson import ObjectId
class PyObjectId(str):
"""Custom ObjectId type for Pydantic"""
@classmethod
def __get_validators__(cls):
yield cls.validate
@classmethod
def validate(cls, v):
if not ObjectId.is_valid(v):
raise ValueError("Invalid ObjectId")
return str(v)
# Auth models
class SendCodeRequest(BaseModel):
email: EmailStr
class RegisterRequest(BaseModel):
email: EmailStr
code: str
password: str
username: str
class LoginRequest(BaseModel):
email: EmailStr
password: str
class TelegramWidgetAuth(BaseModel):
id: int
first_name: str
last_name: Optional[str] = None
username: Optional[str] = None
photo_url: Optional[str] = None
auth_date: int
hash: str
# User models
class UserBase(BaseModel):
id: Optional[str] = Field(None, alias='_id')
username: str
firstName: Optional[str] = None
lastName: Optional[str] = None
photoUrl: Optional[str] = None
role: str = 'user'
telegramId: Optional[str] = None
class Config:
populate_by_name = True
class UserResponse(UserBase):
email: Optional[str] = None
banned: bool = False
createdAt: datetime
lastActiveAt: Optional[datetime] = None
# Post models
class PostBase(BaseModel):
id: Optional[str] = Field(None, alias='_id')
content: Optional[str] = None
images: List[str] = []
tags: List[str] = []
hashtags: List[str] = []
isNSFW: bool = False
isHomo: bool = False
isArt: bool = False
createdAt: datetime
class Config:
populate_by_name = True
class PostResponse(PostBase):
author: Optional[UserBase] = None
likesCount: int = 0
commentsCount: int = 0
publishedToChannel: bool = False
adminNumber: Optional[int] = None
# Report models
class ReportBase(BaseModel):
id: Optional[str] = Field(None, alias='_id')
postId: str
reason: str
status: str = 'pending'
createdAt: datetime
class Config:
populate_by_name = True
class ReportResponse(ReportBase):
reporter: Optional[UserBase] = None
post: Optional[PostResponse] = None
reviewedBy: Optional[UserBase] = None
# Admin models
class AdminBase(BaseModel):
id: Optional[str] = Field(None, alias='_id')
telegramId: str
username: str
firstName: Optional[str] = None
lastName: Optional[str] = None
adminNumber: int
class Config:
populate_by_name = True
class AdminResponse(AdminBase):
addedBy: Optional[str] = None
createdAt: datetime
# Request models
class UpdatePostRequest(BaseModel):
content: Optional[str] = None
tags: Optional[List[str]] = None
isNSFW: Optional[bool] = None
isHomo: Optional[bool] = None
isArt: Optional[bool] = None
class UpdateReportRequest(BaseModel):
status: str = 'reviewed'
class BanUserRequest(BaseModel):
banned: bool
days: Optional[int] = None
class AddAdminRequest(BaseModel):
userId: str
adminNumber: int
class ConfirmAdminRequest(BaseModel):
userId: str
code: str

View File

@ -0,0 +1,40 @@
# FastAPI framework
fastapi==0.109.0
uvicorn[standard]==0.27.0
python-multipart==0.0.6
# Database
motor==3.3.2 # Async MongoDB driver
pymongo==4.6.1
# Authentication & Security
python-jose[cryptography]==3.3.0
passlib[bcrypt]==1.7.4
python-dotenv==1.0.0
pydantic==2.5.3
pydantic-settings==2.1.0
email-validator==2.1.0
# HTTP & WebSocket
httpx==0.26.0
websockets==12.0
python-socketio==5.11.0
# Email
aiosmtplib==3.0.1
aioboto3==12.3.0
boto3==1.34.34
# Redis (optional)
redis==5.0.1
# MinIO (S3)
minio==7.2.3
# Utilities
python-dateutil==2.8.2
validators==0.22.0
# Rate limiting
slowapi==0.1.9

View File

@ -0,0 +1,2 @@
# Routes package

View File

@ -0,0 +1,538 @@
"""
Moderation app routes - main moderation functionality
"""
from datetime import datetime, timedelta
from typing import Optional, List
from fastapi import APIRouter, HTTPException, status, Depends, Query
from bson import ObjectId
from models import (
UserResponse, PostResponse, ReportResponse, AdminResponse,
UpdatePostRequest, UpdateReportRequest, BanUserRequest
)
from database import (
users_collection, posts_collection, reports_collection,
moderation_admins_collection
)
from utils.auth import require_moderator, require_owner, normalize_username
router = APIRouter()
@router.get("/users")
async def get_users(
filter: str = Query('active', description="Filter: active, banned, all"),
page: int = Query(1, ge=1),
limit: int = Query(50, ge=1, le=100),
user: dict = Depends(require_moderator)
):
"""Get users list with filtering"""
try:
query = {}
if filter == 'active':
query['banned'] = {'$ne': True}
elif filter == 'banned':
query['banned'] = True
# 'all' - no filter
skip = (page - 1) * limit
# Get users
cursor = users_collection().find(query).sort('createdAt', -1).skip(skip).limit(limit)
users = await cursor.to_list(length=limit)
# Get total count
total = await users_collection().count_documents(query)
# Serialize users
serialized_users = []
for u in users:
serialized_users.append({
'id': str(u['_id']),
'telegramId': u.get('telegramId'),
'username': u.get('username'),
'firstName': u.get('firstName'),
'lastName': u.get('lastName'),
'photoUrl': u.get('photoUrl'),
'role': u.get('role', 'user'),
'banned': u.get('banned', False),
'bannedUntil': u.get('bannedUntil').isoformat() if u.get('bannedUntil') else None,
'createdAt': u.get('createdAt', datetime.utcnow()).isoformat(),
'lastActiveAt': u.get('lastActiveAt').isoformat() if u.get('lastActiveAt') else None
})
return {
'users': serialized_users,
'total': total,
'totalPages': (total + limit - 1) // limit,
'currentPage': page
}
except Exception as e:
print(f"[ModApp] Ошибка получения пользователей: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Ошибка сервера"
)
@router.put("/users/{user_id}/ban")
async def ban_user(
user_id: str,
request: BanUserRequest,
current_user: dict = Depends(require_moderator)
):
"""Ban or unban user"""
try:
target_user = await users_collection().find_one({'_id': ObjectId(user_id)})
if not target_user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Пользователь не найден"
)
update_data = {'banned': request.banned}
if request.banned and request.days:
update_data['bannedUntil'] = datetime.utcnow() + timedelta(days=request.days)
elif not request.banned:
update_data['bannedUntil'] = None
await users_collection().update_one(
{'_id': ObjectId(user_id)},
{'$set': update_data}
)
return {"success": True}
except HTTPException:
raise
except Exception as e:
print(f"[ModApp] Ошибка бана пользователя: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Ошибка сервера"
)
@router.get("/posts")
async def get_posts(
page: int = Query(1, ge=1),
limit: int = Query(20, ge=1, le=100),
author: Optional[str] = None,
tag: Optional[str] = None,
user: dict = Depends(require_moderator)
):
"""Get posts list with filtering"""
try:
query = {}
if author:
query['author'] = ObjectId(author)
if tag:
query['tags'] = tag
skip = (page - 1) * limit
# Get posts with populated author
pipeline = [
{'$match': query},
{'$sort': {'createdAt': -1}},
{'$skip': skip},
{'$limit': limit},
{
'$lookup': {
'from': 'users',
'localField': 'author',
'foreignField': '_id',
'as': 'author'
}
},
{'$unwind': {'path': '$author', 'preserveNullAndEmptyArrays': True}}
]
posts = await posts_collection().aggregate(pipeline).to_list(length=limit)
total = await posts_collection().count_documents(query)
# Serialize posts
serialized_posts = []
for post in posts:
author_data = None
if post.get('author'):
author_data = {
'id': str(post['author']['_id']),
'username': post['author'].get('username'),
'firstName': post['author'].get('firstName'),
'lastName': post['author'].get('lastName'),
'photoUrl': post['author'].get('photoUrl')
}
serialized_posts.append({
'id': str(post['_id']),
'author': author_data,
'content': post.get('content'),
'hashtags': post.get('hashtags', []),
'tags': post.get('tags', []),
'images': post.get('images', []) or ([post.get('imageUrl')] if post.get('imageUrl') else []),
'commentsCount': len(post.get('comments', [])),
'likesCount': len(post.get('likes', [])),
'isNSFW': post.get('isNSFW', False),
'isArt': post.get('isArt', False),
'publishedToChannel': post.get('publishedToChannel', False),
'adminNumber': post.get('adminNumber'),
'editedAt': post.get('editedAt').isoformat() if post.get('editedAt') else None,
'createdAt': post.get('createdAt', datetime.utcnow()).isoformat()
})
return {
'posts': serialized_posts,
'total': total,
'totalPages': (total + limit - 1) // limit,
'currentPage': page
}
except Exception as e:
print(f"[ModApp] Ошибка получения постов: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Ошибка сервера"
)
@router.get("/posts/{post_id}")
async def get_post(
post_id: str,
user: dict = Depends(require_moderator)
):
"""Get single post with comments"""
try:
pipeline = [
{'$match': {'_id': ObjectId(post_id)}},
{
'$lookup': {
'from': 'users',
'localField': 'author',
'foreignField': '_id',
'as': 'author'
}
},
{'$unwind': {'path': '$author', 'preserveNullAndEmptyArrays': True}}
]
posts = await posts_collection().aggregate(pipeline).to_list(length=1)
if not posts:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Пост не найден"
)
post = posts[0]
# Serialize
author_data = None
if post.get('author'):
author_data = {
'id': str(post['author']['_id']),
'username': post['author'].get('username'),
'firstName': post['author'].get('firstName'),
'lastName': post['author'].get('lastName'),
'photoUrl': post['author'].get('photoUrl')
}
return {
'post': {
'id': str(post['_id']),
'author': author_data,
'content': post.get('content'),
'hashtags': post.get('hashtags', []),
'tags': post.get('tags', []),
'images': post.get('images', []) or ([post.get('imageUrl')] if post.get('imageUrl') else []),
'comments': post.get('comments', []),
'likes': post.get('likes', []),
'isNSFW': post.get('isNSFW', False),
'isArt': post.get('isArt', False),
'publishedToChannel': post.get('publishedToChannel', False),
'adminNumber': post.get('adminNumber'),
'createdAt': post.get('createdAt', datetime.utcnow()).isoformat()
}
}
except HTTPException:
raise
except Exception as e:
print(f"[ModApp] Ошибка получения поста: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Ошибка сервера"
)
@router.put("/posts/{post_id}")
async def update_post(
post_id: str,
request: UpdatePostRequest,
user: dict = Depends(require_moderator)
):
"""Update post"""
try:
post = await posts_collection().find_one({'_id': ObjectId(post_id)})
if not post:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Пост не найден"
)
update_data = {}
if request.content is not None:
update_data['content'] = request.content
if request.tags is not None:
update_data['tags'] = [t.lower() for t in request.tags]
if request.isNSFW is not None:
update_data['isNSFW'] = request.isNSFW
if request.isHomo is not None:
update_data['isHomo'] = request.isHomo
if request.isArt is not None:
update_data['isArt'] = request.isArt
if update_data:
update_data['editedAt'] = datetime.utcnow()
await posts_collection().update_one(
{'_id': ObjectId(post_id)},
{'$set': update_data}
)
return {"success": True}
except HTTPException:
raise
except Exception as e:
print(f"[ModApp] Ошибка обновления поста: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Ошибка сервера"
)
@router.delete("/posts/{post_id}")
async def delete_post(
post_id: str,
user: dict = Depends(require_moderator)
):
"""Delete post"""
try:
post = await posts_collection().find_one({'_id': ObjectId(post_id)})
if not post:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Пост не найден"
)
# Delete post
await posts_collection().delete_one({'_id': ObjectId(post_id)})
return {"success": True, "message": "Пост удален"}
except HTTPException:
raise
except Exception as e:
print(f"[ModApp] Ошибка удаления поста: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Ошибка сервера"
)
@router.get("/reports")
async def get_reports(
status_filter: str = Query('pending', alias='status'),
page: int = Query(1, ge=1),
limit: int = Query(20, ge=1, le=100),
user: dict = Depends(require_moderator)
):
"""Get reports list"""
try:
query = {}
if status_filter != 'all':
query['status'] = status_filter
skip = (page - 1) * limit
# Get reports with populated fields
pipeline = [
{'$match': query},
{'$sort': {'createdAt': -1}},
{'$skip': skip},
{'$limit': limit},
{
'$lookup': {
'from': 'users',
'localField': 'reporter',
'foreignField': '_id',
'as': 'reporter'
}
},
{'$unwind': {'path': '$reporter', 'preserveNullAndEmptyArrays': True}},
{
'$lookup': {
'from': 'posts',
'localField': 'post',
'foreignField': '_id',
'as': 'post'
}
},
{'$unwind': {'path': '$post', 'preserveNullAndEmptyArrays': True}}
]
reports = await reports_collection().aggregate(pipeline).to_list(length=limit)
total = await reports_collection().count_documents(query)
# Serialize
serialized_reports = []
for report in reports:
reporter_data = None
if report.get('reporter'):
reporter_data = {
'id': str(report['reporter']['_id']),
'username': report['reporter'].get('username'),
'firstName': report['reporter'].get('firstName')
}
post_data = None
if report.get('post'):
post_data = {
'id': str(report['post']['_id']),
'content': report['post'].get('content'),
'images': report['post'].get('images', [])
}
serialized_reports.append({
'id': str(report['_id']),
'reporter': reporter_data,
'post': post_data,
'reason': report.get('reason'),
'status': report.get('status'),
'createdAt': report.get('createdAt', datetime.utcnow()).isoformat()
})
return {
'reports': serialized_reports,
'total': total,
'totalPages': (total + limit - 1) // limit,
'currentPage': page
}
except Exception as e:
print(f"[ModApp] Ошибка получения репортов: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Ошибка сервера"
)
@router.put("/reports/{report_id}")
async def update_report(
report_id: str,
request: UpdateReportRequest,
user: dict = Depends(require_moderator)
):
"""Update report status"""
try:
report = await reports_collection().find_one({'_id': ObjectId(report_id)})
if not report:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Репорт не найден"
)
await reports_collection().update_one(
{'_id': ObjectId(report_id)},
{
'$set': {
'status': request.status,
'reviewedBy': user['_id']
}
}
)
return {"success": True}
except HTTPException:
raise
except Exception as e:
print(f"[ModApp] Ошибка обновления репорта: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Ошибка сервера"
)
@router.get("/admins")
async def get_admins(user: dict = Depends(require_moderator)):
"""Get list of moderation admins"""
try:
admins = await moderation_admins_collection().find().sort('adminNumber', 1).to_list(length=100)
serialized_admins = []
for admin in admins:
serialized_admins.append({
'id': str(admin['_id']),
'telegramId': admin.get('telegramId'),
'username': admin.get('username'),
'firstName': admin.get('firstName'),
'lastName': admin.get('lastName'),
'adminNumber': admin.get('adminNumber'),
'addedBy': admin.get('addedBy'),
'createdAt': admin.get('createdAt', datetime.utcnow()).isoformat()
})
return {'admins': serialized_admins}
except Exception as e:
print(f"[ModApp] Ошибка получения админов: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Ошибка получения списка админов"
)
@router.post("/auth/verify")
async def verify_auth(user: dict = Depends(require_moderator)):
"""Verify authentication and get admin list"""
try:
# Get admin list
admins = await moderation_admins_collection().find().sort('adminNumber', 1).to_list(length=100)
admin_list = []
for admin in admins:
admin_list.append({
'adminNumber': admin.get('adminNumber'),
'username': admin.get('username'),
'firstName': admin.get('firstName'),
'telegramId': admin.get('telegramId')
})
return {
'success': True,
'admins': admin_list,
'user': {
'id': str(user['_id']),
'username': user.get('username'),
'role': user.get('role')
}
}
except Exception as e:
print(f"[ModApp] Ошибка верификации: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Ошибка сервера"
)

View File

@ -0,0 +1,439 @@
"""
Moderation authentication routes
"""
import secrets
from datetime import datetime, timedelta
from typing import Optional
from fastapi import APIRouter, HTTPException, status, Response, Cookie, Depends
from fastapi.responses import JSONResponse
from slowapi import Limiter
from slowapi.util import get_remote_address
from bson import ObjectId
from models import (
SendCodeRequest, RegisterRequest, LoginRequest, TelegramWidgetAuth,
UserResponse
)
from database import (
users_collection, email_verification_codes_collection,
moderation_admins_collection
)
from utils.auth import (
hash_password, verify_password,
create_access_token, create_refresh_token,
get_current_user, normalize_username, is_moderation_admin
)
from utils.email_service import send_verification_code
from config import settings
router = APIRouter()
limiter = Limiter(key_func=get_remote_address)
# Rate limiters
AUTH_LIMITER = "5/15minutes" # 5 requests per 15 minutes
CODE_LIMITER = "1/minute" # 1 request per minute
def set_auth_cookies(response: Response, access_token: str, refresh_token: str):
"""Set authentication cookies"""
# Access token cookie (short-lived)
response.set_cookie(
key=settings.JWT_ACCESS_COOKIE_NAME,
value=access_token,
max_age=settings.JWT_ACCESS_EXPIRES_IN,
httponly=True,
secure=settings.IS_PRODUCTION,
samesite='lax'
)
# Refresh token cookie (long-lived)
response.set_cookie(
key=settings.JWT_REFRESH_COOKIE_NAME,
value=refresh_token,
max_age=settings.JWT_REFRESH_EXPIRES_IN,
httponly=True,
secure=settings.IS_PRODUCTION,
samesite='lax'
)
def clear_auth_cookies(response: Response):
"""Clear authentication cookies"""
response.delete_cookie(settings.JWT_ACCESS_COOKIE_NAME)
response.delete_cookie(settings.JWT_REFRESH_COOKIE_NAME)
@router.post("/send-code")
@limiter.limit(CODE_LIMITER)
async def send_code(request: SendCodeRequest):
"""Send verification code to email"""
try:
email_lower = request.email.lower().strip()
# Check if user exists with moderator/admin role
existing_user = await users_collection().find_one({
'email': email_lower,
'role': {'$in': ['moderator', 'admin']}
})
print(f"[ModerationAuth] Проверка пользователя для email {email_lower}: "
f"{{found: {existing_user is not None}, "
f"hasPassword: {bool(existing_user.get('passwordHash')) if existing_user else False}, "
f"role: {existing_user.get('role') if existing_user else None}}}")
# Allow sending code if user exists
if existing_user:
print(f"[ModerationAuth] Пользователь найден, отправка кода разрешена")
else:
# Check if user exists but without proper role
user_by_email = await users_collection().find_one({'email': email_lower})
if user_by_email:
print(f"[ModerationAuth] Пользователь найден, но роль не moderator/admin: {user_by_email.get('role')}")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Регистрация недоступна. Обратитесь к администратору для получения доступа."
)
# No user found - allow for debugging
print(f"[ModerationAuth] Пользователь не найден, но отправка кода разрешена для {email_lower}")
# Generate 6-digit code
code = str(secrets.randbelow(900000) + 100000)
# Delete old codes
await email_verification_codes_collection().delete_many({
'email': email_lower,
'purpose': 'registration'
})
# Save new code (valid for 15 minutes)
await email_verification_codes_collection().insert_one({
'email': email_lower,
'code': code,
'purpose': 'registration',
'verified': False,
'expiresAt': datetime.utcnow() + timedelta(minutes=15),
'createdAt': datetime.utcnow()
})
# Send code via email
try:
await send_verification_code(email_lower, code)
return {"success": True, "message": "Код подтверждения отправлен на email"}
except ValueError as email_error:
# Delete code if email failed
await email_verification_codes_collection().delete_many({
'email': email_lower,
'code': code
})
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(email_error)
)
except Exception as email_error:
await email_verification_codes_collection().delete_many({
'email': email_lower,
'code': code
})
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Не удалось отправить код на email: {str(email_error)}"
)
except HTTPException:
raise
except Exception as e:
print(f"[ModerationAuth] Ошибка отправки кода: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Ошибка сервера"
)
@router.post("/register")
@limiter.limit(AUTH_LIMITER)
async def register(request: RegisterRequest, response: Response):
"""Register with email verification code"""
try:
email_lower = request.email.lower().strip()
# Find verification code
verification_code = await email_verification_codes_collection().find_one({
'email': email_lower,
'code': request.code,
'purpose': 'registration',
'verified': False
})
if not verification_code:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Неверный или истекший код"
)
# Check expiration
if datetime.utcnow() > verification_code['expiresAt']:
await email_verification_codes_collection().delete_one({'_id': verification_code['_id']})
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Код истек. Запросите новый."
)
# Find user (must be created by administrator)
user = await users_collection().find_one({
'email': email_lower,
'role': {'$in': ['moderator', 'admin']}
})
if not user:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Регистрация недоступна. Обратитесь к администратору."
)
# Check if user already has password
if user.get('passwordHash'):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Аккаунт уже зарегистрирован. Используйте вход по паролю."
)
# Check password strength
if len(request.password) < 6:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Пароль должен содержать минимум 6 символов"
)
# Hash password
password_hash = hash_password(request.password)
# Update user
await users_collection().update_one(
{'_id': user['_id']},
{
'$set': {
'passwordHash': password_hash,
'emailVerified': True,
'username': request.username or user.get('username')
}
}
)
# Mark code as verified
await email_verification_codes_collection().update_one(
{'_id': verification_code['_id']},
{'$set': {'verified': True}}
)
# Generate tokens
user_id_str = str(user['_id'])
access_token = create_access_token(user_id_str)
refresh_token = create_refresh_token(user_id_str)
# Set cookies
set_auth_cookies(response, access_token, refresh_token)
return {
"success": True,
"user": {
"id": user_id_str,
"username": request.username or user.get('username'),
"role": user.get('role')
},
"accessToken": access_token
}
except HTTPException:
raise
except Exception as e:
print(f"[ModerationAuth] Ошибка регистрации: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Ошибка сервера"
)
@router.post("/login")
@limiter.limit(AUTH_LIMITER)
async def login(request: LoginRequest, response: Response):
"""Login with email and password"""
try:
email_lower = request.email.lower().strip()
# Find user with password
user = await users_collection().find_one({
'email': email_lower,
'passwordHash': {'$exists': True, '$ne': None},
'role': {'$in': ['moderator', 'admin']}
})
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Неверный email или пароль"
)
if user.get('banned'):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Аккаунт заблокирован"
)
# Verify password
if not verify_password(request.password, user['passwordHash']):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Неверный email или пароль"
)
# Update last active
await users_collection().update_one(
{'_id': user['_id']},
{'$set': {'lastActiveAt': datetime.utcnow()}}
)
# Generate tokens
user_id_str = str(user['_id'])
access_token = create_access_token(user_id_str)
refresh_token = create_refresh_token(user_id_str)
# Set cookies
set_auth_cookies(response, access_token, refresh_token)
return {
"success": True,
"user": {
"id": user_id_str,
"username": user.get('username'),
"role": user.get('role'),
"telegramId": user.get('telegramId')
},
"accessToken": access_token
}
except HTTPException:
raise
except Exception as e:
print(f"[ModerationAuth] Ошибка авторизации: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Ошибка сервера"
)
@router.post("/telegram-widget")
@limiter.limit(AUTH_LIMITER)
async def telegram_widget_auth(request: TelegramWidgetAuth, response: Response):
"""Authenticate via Telegram Login Widget"""
try:
# Find user by telegramId
user = await users_collection().find_one({'telegramId': str(request.id)})
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Пользователь не найден. Сначала зарегистрируйтесь через бота."
)
if user.get('role') not in ['moderator', 'admin']:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Доступ запрещен. У вас нет прав модератора."
)
if user.get('banned'):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Аккаунт заблокирован"
)
# Update user data from widget
update_fields = {}
if request.username and not user.get('username'):
update_fields['username'] = request.username
if request.first_name and not user.get('firstName'):
update_fields['firstName'] = request.first_name
if request.last_name and not user.get('lastName'):
update_fields['lastName'] = request.last_name
if request.photo_url and not user.get('photoUrl'):
update_fields['photoUrl'] = request.photo_url
update_fields['lastActiveAt'] = datetime.utcnow()
if update_fields:
await users_collection().update_one(
{'_id': user['_id']},
{'$set': update_fields}
)
# Generate tokens
user_id_str = str(user['_id'])
access_token = create_access_token(user_id_str)
refresh_token = create_refresh_token(user_id_str)
# Set cookies
set_auth_cookies(response, access_token, refresh_token)
print(f"[ModerationAuth] Успешная авторизация через виджет: {user.get('username')}")
return {
"success": True,
"user": {
"id": user_id_str,
"username": user.get('username'),
"role": user.get('role'),
"telegramId": user.get('telegramId')
},
"accessToken": access_token
}
except HTTPException:
raise
except Exception as e:
print(f"[ModerationAuth] Ошибка авторизации через виджет: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Ошибка сервера"
)
@router.post("/logout")
async def logout(response: Response):
"""Logout and clear cookies"""
clear_auth_cookies(response)
return {"success": True}
@router.get("/config")
async def get_config():
"""Get configuration for frontend"""
bot_username = settings.MODERATION_BOT_USERNAME
# If not set, try to get from Bot API (simplified - can add full implementation)
if not bot_username:
bot_username = "moderation_bot"
return {"botUsername": bot_username}
@router.get("/me")
async def get_current_user_info(user: dict = Depends(get_current_user)):
"""Get current authenticated user info"""
if user.get('role') not in ['moderator', 'admin']:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Доступ запрещен"
)
return {
"success": True,
"user": {
"id": str(user['_id']),
"username": user.get('username'),
"role": user.get('role'),
"telegramId": user.get('telegramId')
}
}

19
moderation/backend-py/run.py Executable file
View File

@ -0,0 +1,19 @@
#!/usr/bin/env python3
"""
Run script for Nakama Moderation Backend
"""
import uvicorn
from config import settings, print_config
if __name__ == "__main__":
print_config()
uvicorn.run(
"main:app",
host="0.0.0.0",
port=settings.MODERATION_PORT,
reload=settings.IS_DEVELOPMENT,
log_level="info",
access_log=True
)

31
moderation/backend-py/start.sh Executable file
View File

@ -0,0 +1,31 @@
#!/bin/bash
# Start script for Nakama Moderation Backend (Python)
echo "🚀 Запуск Nakama Moderation Backend (Python)..."
# Check if .env exists
if [ ! -f "../../.env" ]; then
echo "❌ Файл .env не найден в корне проекта!"
echo " Создайте файл nakama/.env с настройками"
exit 1
fi
# Check if venv exists
if [ ! -d "venv" ]; then
echo "📦 Создание виртуального окружения..."
python3 -m venv venv
fi
# Activate venv
echo "🔧 Активация виртуального окружения..."
source venv/bin/activate
# Install/update dependencies
echo "📥 Установка зависимостей..."
pip install -q -r requirements.txt
# Run server
echo "✅ Запуск сервера..."
python main.py

View File

@ -0,0 +1,56 @@
#!/usr/bin/env python3
"""
Test email sending
"""
import asyncio
import sys
async def test_email():
"""Test email configuration"""
from config import settings
from utils.email_service import send_verification_code
print("\n" + "=" * 60)
print("📧 Тест отправки email")
print("=" * 60)
print(f"\nНастройки:")
print(f" Provider: {settings.EMAIL_PROVIDER}")
print(f" Host: {settings.YANDEX_SMTP_HOST}")
print(f" Port: {settings.YANDEX_SMTP_PORT}")
print(f" User: {settings.YANDEX_SMTP_USER}")
print(f" Has Password: {bool(settings.YANDEX_SMTP_PASSWORD)}")
print(f" From: {settings.EMAIL_FROM}")
if not settings.YANDEX_SMTP_USER or not settings.YANDEX_SMTP_PASSWORD:
print("\n❌ YANDEX_SMTP_USER или YANDEX_SMTP_PASSWORD не установлены в .env!")
print(" Проверьте файл nakama/.env")
sys.exit(1)
# Ask for email
test_email = input("\nВведите email для теста (или Enter для пропуска): ").strip()
if not test_email:
print("Тест пропущен")
return
print(f"\n📤 Отправка тестового кода на {test_email}...")
try:
await send_verification_code(test_email, "123456")
print("\n✅ Email успешно отправлен!")
print(f" Проверьте почту: {test_email}")
except Exception as e:
print(f"\n❌ Ошибка отправки: {e}")
print("\nПроверьте:")
print(" 1. Используете ли пароль приложения (не основной пароль)")
print(" 2. Правильность email в YANDEX_SMTP_USER")
print(" 3. Доступность smtp.yandex.ru:465")
sys.exit(1)
print("\n" + "=" * 60)
if __name__ == "__main__":
asyncio.run(test_email())

View File

@ -0,0 +1,2 @@
# Utils package

View File

@ -0,0 +1,186 @@
"""
Authentication utilities
"""
from datetime import datetime, timedelta
from typing import Optional
from jose import JWTError, jwt
from passlib.context import CryptContext
from fastapi import HTTPException, status, Depends, Cookie, Header
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from config import settings
from database import users_collection, moderation_admins_collection
# Password hashing
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
# JWT bearer
security = HTTPBearer(auto_error=False)
def hash_password(password: str) -> str:
"""Hash password with bcrypt"""
return pwd_context.hash(password)
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""Verify password against hash"""
return pwd_context.verify(plain_password, hashed_password)
def create_access_token(user_id: str, expires_delta: Optional[timedelta] = None) -> str:
"""Create JWT access token"""
if expires_delta is None:
expires_delta = timedelta(seconds=settings.JWT_ACCESS_EXPIRES_IN)
expire = datetime.utcnow() + expires_delta
to_encode = {
"userId": user_id,
"exp": expire,
"type": "access"
}
encoded_jwt = jwt.encode(to_encode, settings.JWT_ACCESS_SECRET, algorithm="HS256")
return encoded_jwt
def create_refresh_token(user_id: str, expires_delta: Optional[timedelta] = None) -> str:
"""Create JWT refresh token"""
if expires_delta is None:
expires_delta = timedelta(seconds=settings.JWT_REFRESH_EXPIRES_IN)
expire = datetime.utcnow() + expires_delta
to_encode = {
"userId": user_id,
"exp": expire,
"type": "refresh"
}
encoded_jwt = jwt.encode(to_encode, settings.JWT_REFRESH_SECRET, algorithm="HS256")
return encoded_jwt
def verify_token(token: str, token_type: str = "access") -> Optional[dict]:
"""Verify JWT token"""
try:
secret = settings.JWT_ACCESS_SECRET if token_type == "access" else settings.JWT_REFRESH_SECRET
payload = jwt.decode(token, secret, algorithms=["HS256"])
if payload.get("type") != token_type:
return None
return payload
except JWTError:
return None
async def get_current_user(
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
access_token: Optional[str] = Cookie(None, alias=settings.JWT_ACCESS_COOKIE_NAME)
):
"""Get current authenticated user"""
token = None
# Try Bearer token from header
if credentials:
token = credentials.credentials
# Try cookie
elif access_token:
token = access_token
if not token:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Не авторизован"
)
# Verify token
payload = verify_token(token, "access")
if not payload:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Неверный токен"
)
# Get user from database
user_id = payload.get("userId")
if not user_id:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Неверный токен"
)
from bson import ObjectId
user = await users_collection().find_one({"_id": ObjectId(user_id)})
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Пользователь не найден"
)
if user.get('banned'):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Аккаунт заблокирован"
)
return user
async def require_moderator(user: dict = Depends(get_current_user)):
"""Require moderator or admin role"""
if user.get('role') not in ['moderator', 'admin']:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Недостаточно прав для модерации"
)
return user
async def require_owner(user: dict = Depends(get_current_user)):
"""Require owner role"""
username = user.get('username', '').lower()
is_owner = (
user.get('role') == 'admin' or
username in settings.OWNER_USERNAMES_LIST
)
if not is_owner:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Недостаточно прав. Требуются права владельца."
)
return user
def normalize_username(username: Optional[str]) -> Optional[str]:
"""Normalize username (remove @ and lowercase)"""
if not username:
return None
username = username.strip().lower()
if username.startswith('@'):
username = username[1:]
return username if username else None
async def is_moderation_admin(telegram_id: Optional[str] = None, username: Optional[str] = None) -> bool:
"""Check if user is moderation admin"""
if not telegram_id and not username:
return False
query = {}
if telegram_id:
query['telegramId'] = telegram_id
if username:
normalized = normalize_username(username)
if normalized:
query['username'] = normalized
admin = await moderation_admins_collection().find_one(query)
return admin is not None

View File

@ -0,0 +1,154 @@
"""
Email sending utilities with support for Yandex SMTP
"""
import smtplib
import logging
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from typing import Optional
from config import settings
logger = logging.getLogger(__name__)
def generate_verification_email(code: str) -> str:
"""Generate HTML email with verification code"""
return f"""
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body {{ font-family: Arial, sans-serif; line-height: 1.6; color: #333; }}
.container {{ max-width: 600px; margin: 0 auto; padding: 20px; }}
.code {{ font-size: 32px; font-weight: bold; color: #007bff;
text-align: center; padding: 20px; background: #f8f9fa;
border-radius: 8px; margin: 20px 0; letter-spacing: 8px; }}
.footer {{ margin-top: 30px; font-size: 12px; color: #666; }}
</style>
</head>
<body>
<div class="container">
<h1>Код подтверждения</h1>
<p>Ваш код для регистрации в Nakama:</p>
<div class="code">{code}</div>
<p>Код действителен в течение 15 минут.</p>
<div class="footer">
<p>Если вы не запрашивали этот код, просто проигнорируйте это письмо.</p>
</div>
</div>
</body>
</html>
"""
async def send_email_smtp(to: str, subject: str, html: str, text: Optional[str] = None):
"""Send email via SMTP (Yandex, Gmail, etc.)"""
try:
# Create message
msg = MIMEMultipart('alternative')
msg['Subject'] = subject
msg['From'] = settings.EMAIL_FROM
msg['To'] = to
# Add text and HTML parts
if text:
msg.attach(MIMEText(text, 'plain', 'utf-8'))
msg.attach(MIMEText(html, 'html', 'utf-8'))
# Connect and send
if settings.EMAIL_PROVIDER == 'yandex':
logger.info(f"[Email] Отправка через Yandex SMTP: {settings.YANDEX_SMTP_HOST}:{settings.YANDEX_SMTP_PORT}")
if not settings.YANDEX_SMTP_USER or not settings.YANDEX_SMTP_PASSWORD:
raise ValueError("YANDEX_SMTP_USER и YANDEX_SMTP_PASSWORD должны быть установлены в .env")
# Используем SMTP_SSL для порта 465
if settings.YANDEX_SMTP_PORT == 465:
server = smtplib.SMTP_SSL(settings.YANDEX_SMTP_HOST, settings.YANDEX_SMTP_PORT)
else:
server = smtplib.SMTP(settings.YANDEX_SMTP_HOST, settings.YANDEX_SMTP_PORT)
if settings.YANDEX_SMTP_SECURE:
server.starttls()
server.login(settings.YANDEX_SMTP_USER, settings.YANDEX_SMTP_PASSWORD)
server.send_message(msg)
server.quit()
logger.info(f"✅ Email отправлен на {to}")
return {"success": True, "to": to}
else:
raise ValueError(f"Email provider '{settings.EMAIL_PROVIDER}' не поддерживается. Используйте 'yandex'")
except Exception as e:
logger.error(f"❌ Ошибка отправки email: {e}")
# More informative error messages
if "Authentication" in str(e) or "credentials" in str(e).lower():
raise ValueError(
"Неверные учетные данные SMTP. Для Yandex используйте пароль приложения "
"(https://id.yandex.ru/security), а не основной пароль аккаунта."
)
elif "Connection" in str(e):
raise ValueError(
f"Не удалось подключиться к SMTP серверу {settings.YANDEX_SMTP_HOST}:{settings.YANDEX_SMTP_PORT}"
)
raise
async def send_verification_code(email: str, code: str):
"""Send verification code to email"""
subject = "Код подтверждения регистрации - Nakama"
html = generate_verification_email(code)
text = f"Ваш код подтверждения: {code}. Код действителен 15 минут."
return await send_email_smtp(email, subject, html, text)
async def send_admin_confirmation_code(code: str, action: str, user_info: dict):
"""Send admin confirmation code to owner email"""
action_text = "добавления админа" if action == "add" else "удаления админа"
subject = f"Код подтверждения {action_text} - Nakama Moderation"
html = f"""
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body {{ font-family: Arial, sans-serif; line-height: 1.6; color: #333; }}
.container {{ max-width: 600px; margin: 0 auto; padding: 20px; }}
.code {{ font-size: 32px; font-weight: bold; color: #007bff;
text-align: center; padding: 20px; background: #f8f9fa;
border-radius: 8px; margin: 20px 0; letter-spacing: 8px; }}
.info {{ background: #e7f3ff; padding: 15px; border-radius: 8px; margin: 20px 0; }}
.footer {{ margin-top: 30px; font-size: 12px; color: #666; }}
</style>
</head>
<body>
<div class="container">
<h1>Подтверждение {action_text}</h1>
<p><strong>Пользователь:</strong> @{user_info.get('username', 'не указан')} ({user_info.get('firstName', '')})</p>
{f"<p><strong>Номер админа:</strong> {user_info['adminNumber']}</p>" if 'adminNumber' in user_info else ''}
<div class="info">
<p><strong>Код подтверждения:</strong></p>
<div class="code">{code}</div>
<p>Код действителен в течение 5 минут.</p>
</div>
<div class="footer">
<p>Если вы не запрашивали это подтверждение, проигнорируйте это письмо.</p>
</div>
</div>
</body>
</html>
"""
text = f"""Код подтверждения {action_text}: {code}
Пользователь: @{user_info.get('username', 'не указан')}
Код действителен 5 минут."""
return await send_email_smtp(settings.OWNER_EMAIL, subject, html, text)

View File

@ -0,0 +1,68 @@
"""
MinIO client for file storage
"""
from typing import Optional
from minio import Minio
from minio.error import S3Error
import logging
from config import settings
logger = logging.getLogger(__name__)
minio_client: Optional[Minio] = None
def init_minio():
"""Initialize MinIO client"""
global minio_client
if not settings.MINIO_ENABLED:
logger.info("MinIO отключен")
return None
try:
endpoint = f"{settings.MINIO_ENDPOINT}:{settings.MINIO_PORT}"
minio_client = Minio(
endpoint,
access_key=settings.MINIO_ACCESS_KEY,
secret_key=settings.MINIO_SECRET_KEY,
secure=settings.MINIO_USE_SSL
)
# Test connection
if not minio_client.bucket_exists(settings.MINIO_BUCKET):
logger.warning(f"Bucket '{settings.MINIO_BUCKET}' не существует")
else:
logger.info(f"✅ MinIO подключен: {endpoint}, bucket: {settings.MINIO_BUCKET}")
return minio_client
except S3Error as e:
logger.error(f"❌ Ошибка подключения к MinIO: {e}")
return None
except Exception as e:
logger.error(f"❌ Ошибка инициализации MinIO: {e}")
return None
def get_minio_client():
"""Get MinIO client instance"""
return minio_client
async def delete_file(file_path: str):
"""Delete file from MinIO"""
if not minio_client:
logger.warning("MinIO client not initialized")
return False
try:
minio_client.remove_object(settings.MINIO_BUCKET, file_path)
logger.info(f"✅ Файл удален из MinIO: {file_path}")
return True
except S3Error as e:
logger.error(f"❌ Ошибка удаления файла из MinIO: {e}")
return False

View File

@ -0,0 +1,127 @@
"""
WebSocket server for moderation chat
"""
import socketio
import logging
from typing import Dict, Set
logger = logging.getLogger(__name__)
# Create Socket.IO server
sio = socketio.AsyncServer(
async_mode='asgi',
cors_allowed_origins='*',
logger=False,
engineio_logger=False
)
# Track connected moderators
connected_moderators: Dict[str, Set[str]] = {} # {room: {sid1, sid2, ...}}
@sio.event
async def connect(sid, environ):
"""Handle client connection"""
logger.info(f"[WebSocket] Client connected: {sid}")
@sio.event
async def disconnect(sid):
"""Handle client disconnection"""
logger.info(f"[WebSocket] Client disconnected: {sid}")
# Remove from all rooms
for room, sids in connected_moderators.items():
if sid in sids:
sids.remove(sid)
await sio.emit('user_left', {'sid': sid}, room=room, skip_sid=sid)
@sio.event
async def join_moderation_chat(sid, data):
"""Join moderation chat room"""
try:
user_id = data.get('userId')
username = data.get('username')
if not user_id:
return
room = 'moderation_chat'
await sio.enter_room(sid, room)
if room not in connected_moderators:
connected_moderators[room] = set()
connected_moderators[room].add(sid)
logger.info(f"[WebSocket] {username} ({user_id}) joined moderation chat")
# Notify others
await sio.emit('user_joined', {
'userId': user_id,
'username': username,
'sid': sid
}, room=room, skip_sid=sid)
except Exception as e:
logger.error(f"[WebSocket] Error joining chat: {e}")
@sio.event
async def leave_moderation_chat(sid, data):
"""Leave moderation chat room"""
try:
room = 'moderation_chat'
await sio.leave_room(sid, room)
if room in connected_moderators and sid in connected_moderators[room]:
connected_moderators[room].remove(sid)
# Notify others
await sio.emit('user_left', {'sid': sid}, room=room, skip_sid=sid)
except Exception as e:
logger.error(f"[WebSocket] Error leaving chat: {e}")
@sio.event
async def moderation_message(sid, data):
"""Handle moderation chat message"""
try:
room = 'moderation_chat'
message_data = {
'userId': data.get('userId'),
'username': data.get('username'),
'message': data.get('message'),
'timestamp': data.get('timestamp')
}
# Broadcast to all in room
await sio.emit('moderation_message', message_data, room=room)
logger.info(f"[WebSocket] Message from {data.get('username')}: {data.get('message')[:50]}...")
except Exception as e:
logger.error(f"[WebSocket] Error sending message: {e}")
@sio.event
async def typing(sid, data):
"""Handle typing indicator"""
try:
room = 'moderation_chat'
await sio.emit('typing', {
'userId': data.get('userId'),
'username': data.get('username'),
'isTyping': data.get('isTyping', True)
}, room=room, skip_sid=sid)
except Exception as e:
logger.error(f"[WebSocket] Error handling typing: {e}")
def get_socketio_app():
"""Get Socket.IO ASGI app"""
return socketio.ASGIApp(sio)