Update files
This commit is contained in:
parent
fb12c0626b
commit
ed85d8f6db
|
|
@ -0,0 +1,332 @@
|
|||
# 🔴 Решение проблемы MongoDB Connection
|
||||
|
||||
## Проблема
|
||||
```
|
||||
MongoServerSelectionError: connect ECONNREFUSED 103.80.87.247:27017
|
||||
```
|
||||
|
||||
Сервер не может подключиться к MongoDB на `103.80.87.247:27017`.
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Диагностика
|
||||
|
||||
### 1. Подключитесь к серверу
|
||||
```bash
|
||||
ssh user@103.80.87.247
|
||||
```
|
||||
|
||||
### 2. Проверьте, запущен ли MongoDB
|
||||
```bash
|
||||
# Проверка статуса
|
||||
sudo systemctl status mongod
|
||||
# или
|
||||
sudo systemctl status mongodb
|
||||
|
||||
# Если не запущен - запустите
|
||||
sudo systemctl start mongod
|
||||
sudo systemctl enable mongod # автозапуск
|
||||
```
|
||||
|
||||
### 3. Проверьте порт 27017
|
||||
```bash
|
||||
# Слушает ли MongoDB порт?
|
||||
sudo netstat -tlnp | grep 27017
|
||||
# или
|
||||
sudo ss -tlnp | grep 27017
|
||||
|
||||
# Проверка соединения локально
|
||||
mongo --eval "db.version()"
|
||||
# или для новых версий MongoDB
|
||||
mongosh --eval "db.version()"
|
||||
```
|
||||
|
||||
### 4. Проверьте конфигурацию MongoDB
|
||||
```bash
|
||||
# Откройте конфиг
|
||||
sudo nano /etc/mongod.conf
|
||||
|
||||
# Найдите секцию net:
|
||||
# net:
|
||||
# port: 27017
|
||||
# bindIp: 127.0.0.1 # <-- ПРОБЛЕМА! Слушает только localhost
|
||||
|
||||
# Измените на:
|
||||
# net:
|
||||
# port: 27017
|
||||
# bindIp: 0.0.0.0 # Слушать все интерфейсы
|
||||
```
|
||||
|
||||
### 5. Перезапустите MongoDB
|
||||
```bash
|
||||
sudo systemctl restart mongod
|
||||
|
||||
# Проверьте снова
|
||||
sudo netstat -tlnp | grep 27017
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Решения
|
||||
|
||||
### Решение 1: MongoDB на том же сервере (локально)
|
||||
|
||||
Если ваше приложение **работает на том же сервере** (103.80.87.247), используйте **localhost**:
|
||||
|
||||
#### В Docker (docker-compose.yml)
|
||||
```yaml
|
||||
environment:
|
||||
- MONGODB_URI=mongodb://localhost:27017/nakama
|
||||
```
|
||||
|
||||
#### Или в .env файле
|
||||
```bash
|
||||
MONGODB_URI=mongodb://localhost:27017/nakama
|
||||
```
|
||||
|
||||
#### Если MongoDB в Docker контейнере
|
||||
```bash
|
||||
# В docker-compose.yml используйте имя сервиса:
|
||||
MONGODB_URI=mongodb://mongo:27017/nakama
|
||||
|
||||
# Где mongo - имя сервиса MongoDB в docker-compose.yml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Решение 2: Настроить MongoDB для удаленного доступа
|
||||
|
||||
Если MongoDB на отдельном сервере:
|
||||
|
||||
#### 1. Измените конфиг MongoDB
|
||||
```bash
|
||||
sudo nano /etc/mongod.conf
|
||||
```
|
||||
|
||||
```yaml
|
||||
# /etc/mongod.conf
|
||||
net:
|
||||
port: 27017
|
||||
bindIp: 0.0.0.0 # Слушать все интерфейсы
|
||||
|
||||
security:
|
||||
authorization: enabled # Включить авторизацию!
|
||||
```
|
||||
|
||||
#### 2. Создайте пользователя
|
||||
```bash
|
||||
mongosh
|
||||
```
|
||||
|
||||
```javascript
|
||||
use admin
|
||||
db.createUser({
|
||||
user: "nakama_admin",
|
||||
pwd: "СИЛЬНЫЙ_ПАРОЛЬ_ЗДЕСЬ",
|
||||
roles: [
|
||||
{ role: "readWrite", db: "nakama" },
|
||||
{ role: "dbAdmin", db: "nakama" }
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
#### 3. Обновите connection string
|
||||
```bash
|
||||
# В .env или docker-compose.yml
|
||||
MONGODB_URI=mongodb://nakama_admin:ПАРОЛЬ@103.80.87.247:27017/nakama?authSource=admin
|
||||
```
|
||||
|
||||
#### 4. Настройте Firewall
|
||||
```bash
|
||||
# UFW
|
||||
sudo ufw allow 27017/tcp
|
||||
sudo ufw reload
|
||||
|
||||
# iptables
|
||||
sudo iptables -A INPUT -p tcp --dport 27017 -j ACCEPT
|
||||
sudo iptables-save
|
||||
```
|
||||
|
||||
⚠️ **ВАЖНО:** Открытый MongoDB без пароля - **огромная дыра в безопасности**!
|
||||
|
||||
---
|
||||
|
||||
### Решение 3: Использовать MongoDB Atlas (Рекомендуется) ☁️
|
||||
|
||||
Самый безопасный и простой вариант:
|
||||
|
||||
#### 1. Создайте кластер
|
||||
1. Зайдите на https://www.mongodb.com/cloud/atlas
|
||||
2. Создайте бесплатный M0 кластер
|
||||
3. Создайте пользователя БД
|
||||
4. Добавьте IP сервера в Network Access (или `0.0.0.0/0` для всех)
|
||||
|
||||
#### 2. Получите connection string
|
||||
```
|
||||
mongodb+srv://username:password@cluster.mongodb.net/nakama?retryWrites=true&w=majority
|
||||
```
|
||||
|
||||
#### 3. Обновите конфигурацию
|
||||
```bash
|
||||
# .env или docker-compose.yml
|
||||
MONGODB_URI=mongodb+srv://username:password@cluster.mongodb.net/nakama?retryWrites=true&w=majority
|
||||
```
|
||||
|
||||
#### 4. Перезапустите приложение
|
||||
```bash
|
||||
docker-compose down
|
||||
docker-compose up -d
|
||||
# или
|
||||
pm2 restart all
|
||||
```
|
||||
|
||||
✅ **Преимущества Atlas:**
|
||||
- Автоматические бэкапы
|
||||
- Мониторинг
|
||||
- Безопасность из коробки
|
||||
- Бесплатный tier (512 MB)
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Быстрое решение (для теста)
|
||||
|
||||
Если MongoDB **на том же сервере**, просто замените IP на localhost:
|
||||
|
||||
```bash
|
||||
# Найдите, где запущено приложение (Docker или PM2)
|
||||
docker ps
|
||||
# или
|
||||
pm2 list
|
||||
|
||||
# Остановите
|
||||
docker-compose down
|
||||
# или
|
||||
pm2 stop all
|
||||
|
||||
# Отредактируйте docker-compose.yml или .env:
|
||||
nano docker-compose.yml
|
||||
|
||||
# Замените:
|
||||
MONGODB_URI=mongodb://103.80.87.247:27017/nakama
|
||||
# на:
|
||||
MONGODB_URI=mongodb://localhost:27017/nakama
|
||||
# или для Docker:
|
||||
MONGODB_URI=mongodb://mongo:27017/nakama
|
||||
|
||||
# Запустите снова
|
||||
docker-compose up -d
|
||||
# или
|
||||
pm2 start all
|
||||
|
||||
# Проверьте логи
|
||||
docker-compose logs -f backend
|
||||
# или
|
||||
pm2 logs
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🐳 Docker-compose пример
|
||||
|
||||
Если используете Docker Compose:
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# MongoDB сервис
|
||||
mongo:
|
||||
image: mongo:7
|
||||
restart: always
|
||||
volumes:
|
||||
- mongo-data:/data/db
|
||||
ports:
|
||||
- "27017:27017"
|
||||
environment:
|
||||
MONGO_INITDB_ROOT_USERNAME: admin
|
||||
MONGO_INITDB_ROOT_PASSWORD: secure_password_here
|
||||
MONGO_INITDB_DATABASE: nakama
|
||||
|
||||
# Backend
|
||||
backend:
|
||||
build: ./backend
|
||||
depends_on:
|
||||
- mongo
|
||||
environment:
|
||||
# Используйте имя сервиса 'mongo'
|
||||
- MONGODB_URI=mongodb://admin:secure_password_here@mongo:27017/nakama?authSource=admin
|
||||
- PORT=3000
|
||||
ports:
|
||||
- "3000:3000"
|
||||
|
||||
volumes:
|
||||
mongo-data:
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Проверка после исправления
|
||||
|
||||
```bash
|
||||
# Проверьте логи приложения
|
||||
docker-compose logs -f backend
|
||||
# или
|
||||
pm2 logs
|
||||
|
||||
# Должны увидеть:
|
||||
# ✅ MongoDB подключена
|
||||
# ✅ Сервер запущен на порту 3000
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 Текущая конфигурация
|
||||
|
||||
Судя по вашим логам:
|
||||
- **Сервер:** 103.80.87.247
|
||||
- **MongoDB:** пытается подключиться к 103.80.87.247:27017
|
||||
- **Проблема:** MongoDB недоступен на этом адресе
|
||||
|
||||
**Скорее всего:**
|
||||
1. MongoDB слушает только localhost (127.0.0.1)
|
||||
2. Или MongoDB не запущен
|
||||
3. Или нужно использовать внутренний IP/hostname
|
||||
|
||||
---
|
||||
|
||||
## ⚡ Быстрый чеклист
|
||||
|
||||
- [ ] MongoDB запущен? `sudo systemctl status mongod`
|
||||
- [ ] Порт 27017 слушается? `sudo netstat -tlnp | grep 27017`
|
||||
- [ ] bindIp настроен? Проверьте `/etc/mongod.conf`
|
||||
- [ ] Firewall пропускает? `sudo ufw status`
|
||||
- [ ] Правильный connection string в .env?
|
||||
- [ ] Приложение перезапущено после изменений?
|
||||
|
||||
---
|
||||
|
||||
## 🆘 Если ничего не помогло
|
||||
|
||||
1. **Покажите вывод:**
|
||||
```bash
|
||||
sudo systemctl status mongod
|
||||
sudo netstat -tlnp | grep 27017
|
||||
cat /etc/mongod.conf | grep -A5 "net:"
|
||||
```
|
||||
|
||||
2. **Проверьте переменные окружения:**
|
||||
```bash
|
||||
# Если Docker
|
||||
docker exec <container_name> env | grep MONGODB
|
||||
|
||||
# Если PM2
|
||||
pm2 env <app_name>
|
||||
```
|
||||
|
||||
3. **Используйте MongoDB Atlas** (самый простой вариант)
|
||||
|
||||
---
|
||||
|
||||
**Рекомендация:** Используйте **MongoDB Atlas** для production - это безопасно, надежно и бесплатно для малых проектов!
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
# Исправление ошибки 403 в MinIO
|
||||
|
||||
## 🔴 Проблема
|
||||
```
|
||||
Failed to load resource: the server responded with a status of 403 ()
|
||||
```
|
||||
|
||||
Это означает, что bucket `nakama-media` не публичный и браузер не может загрузить изображения.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Быстрое решение (через MinIO Console)
|
||||
|
||||
### Шаг 1: Откройте консоль MinIO
|
||||
```
|
||||
http://103.80.87.247:9901/
|
||||
```
|
||||
|
||||
### Шаг 2: Войдите
|
||||
- **Username**: `minioadmin` (или ваш логин)
|
||||
- **Password**: `minioadmin` (или ваш пароль)
|
||||
|
||||
### Шаг 3: Настройте публичный доступ
|
||||
1. В боковом меню выберите **Buckets**
|
||||
2. Найдите **nakama-media**
|
||||
3. Нажмите на имя bucket
|
||||
4. Перейдите на вкладку **Anonymous**
|
||||
5. Нажмите **Add Access Rule**
|
||||
6. Введите префикс: `*` (для всех файлов)
|
||||
7. Права доступа: выберите **readonly** или **download**
|
||||
8. Нажмите **Save**
|
||||
|
||||
---
|
||||
|
||||
## ✅ Альтернатива: Через MinIO Client (mc)
|
||||
|
||||
### На сервере с MinIO выполните:
|
||||
|
||||
```bash
|
||||
# Установите mc
|
||||
curl -O https://dl.min.io/client/mc/release/linux-amd64/mc
|
||||
chmod +x mc
|
||||
sudo mv mc /usr/local/bin/
|
||||
|
||||
# Настройте подключение
|
||||
mc alias set myminio http://localhost:9000 minioadmin minioadmin
|
||||
|
||||
# Сделайте bucket публичным
|
||||
mc anonymous set download myminio/nakama-media
|
||||
|
||||
# Проверьте
|
||||
mc anonymous get myminio/nakama-media
|
||||
```
|
||||
|
||||
Должно вывести: `Access permission for 'myminio/nakama-media' is 'download'`
|
||||
|
||||
---
|
||||
|
||||
## ✅ Автоматический скрипт
|
||||
|
||||
```bash
|
||||
bash fix-minio-public.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 Проверьте .env
|
||||
|
||||
Убедитесь, что в `.env` (в корне проекта) установлено:
|
||||
|
||||
```env
|
||||
MINIO_ENABLED=true
|
||||
MINIO_ENDPOINT=103.80.87.247
|
||||
MINIO_PORT=9000
|
||||
MINIO_USE_SSL=false
|
||||
MINIO_PUBLIC_BUCKET=true
|
||||
MINIO_ACCESS_KEY=minioadmin
|
||||
MINIO_SECRET_KEY=minioadmin
|
||||
MINIO_BUCKET=nakama-media
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Перезапустите backend
|
||||
|
||||
```bash
|
||||
docker compose restart backend
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Проверка
|
||||
|
||||
Откройте в браузере:
|
||||
```
|
||||
http://103.80.87.247:9000/nakama-media/posts/test.jpg
|
||||
```
|
||||
|
||||
Если файл существует, он должен загрузиться без ошибок.
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Если используете Nginx (minio.glpshchn.ru)
|
||||
|
||||
Убедитесь, что:
|
||||
1. **MINIO_ENDPOINT** = `minio.glpshchn.ru`
|
||||
2. **MINIO_PORT** = `443`
|
||||
3. **MINIO_USE_SSL** = `true`
|
||||
4. **MINIO_PUBLIC_URL** = `https://minio.glpshchn.ru`
|
||||
|
||||
И перезапустите backend!
|
||||
|
||||
|
||||
|
|
@ -119,7 +119,8 @@ function scheduleAvatarUpdates() {
|
|||
|
||||
module.exports = {
|
||||
scheduleAvatarUpdates,
|
||||
updateAllUserAvatars
|
||||
updateAllUserAvatars,
|
||||
fetchLatestAvatar
|
||||
};
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ const User = require('../models/User');
|
|||
const { validateTelegramId } = require('./validator');
|
||||
const { logSecurityEvent } = require('./logger');
|
||||
const { validateAndParseInitData } = require('../utils/telegram');
|
||||
const { fetchLatestAvatar } = require('../jobs/avatarUpdater');
|
||||
|
||||
const OFFICIAL_CLIENT_MESSAGE = 'Используйте официальный клиент. Сообщите об ошибке в https://t.me/NakamaReportbot';
|
||||
const ALLOWED_SEARCH_PREFERENCES = ['furry', 'anime'];
|
||||
|
|
@ -47,6 +48,61 @@ const ensureUserSettings = async (user) => {
|
|||
}
|
||||
};
|
||||
|
||||
// Подтянуть отсутствующие данные пользователя из Telegram
|
||||
const ensureUserData = async (user, telegramUser) => {
|
||||
if (!user || !telegramUser) return;
|
||||
|
||||
let updated = false;
|
||||
|
||||
// Обновить username, если отсутствует или пустой
|
||||
if (!user.username || user.username.trim() === '') {
|
||||
if (telegramUser.username) {
|
||||
user.username = telegramUser.username;
|
||||
updated = true;
|
||||
} else if (telegramUser.first_name) {
|
||||
user.username = telegramUser.first_name;
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Обновить firstName, если отсутствует
|
||||
if (!user.firstName && telegramUser.first_name) {
|
||||
user.firstName = telegramUser.first_name;
|
||||
updated = true;
|
||||
}
|
||||
|
||||
// Обновить lastName, если отсутствует
|
||||
if (user.lastName === undefined || user.lastName === null) {
|
||||
user.lastName = telegramUser.last_name || '';
|
||||
updated = true;
|
||||
}
|
||||
|
||||
// Обновить аватарку, если отсутствует
|
||||
if (!user.photoUrl) {
|
||||
// Сначала проверить photo_url из initData
|
||||
if (telegramUser.photo_url) {
|
||||
user.photoUrl = telegramUser.photo_url;
|
||||
updated = true;
|
||||
} else {
|
||||
// Если нет в initData, попробовать получить через Bot API
|
||||
try {
|
||||
const avatarUrl = await fetchLatestAvatar(user.telegramId);
|
||||
if (avatarUrl) {
|
||||
user.photoUrl = avatarUrl;
|
||||
updated = true;
|
||||
}
|
||||
} catch (error) {
|
||||
// Игнорируем ошибки получения аватарки
|
||||
console.log('Не удалось получить аватарку через Bot API:', error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (updated) {
|
||||
await user.save();
|
||||
}
|
||||
};
|
||||
|
||||
const authenticate = async (req, res, next) => {
|
||||
try {
|
||||
const authHeader = req.headers.authorization || '';
|
||||
|
|
@ -94,19 +150,34 @@ const authenticate = async (req, res, next) => {
|
|||
if (!user) {
|
||||
user = new User({
|
||||
telegramId: telegramUser.id.toString(),
|
||||
username: telegramUser.username || telegramUser.first_name,
|
||||
firstName: telegramUser.first_name,
|
||||
lastName: telegramUser.last_name,
|
||||
photoUrl: telegramUser.photo_url
|
||||
username: telegramUser.username || telegramUser.first_name || 'user',
|
||||
firstName: telegramUser.first_name || '',
|
||||
lastName: telegramUser.last_name || '',
|
||||
photoUrl: telegramUser.photo_url || null
|
||||
});
|
||||
await user.save();
|
||||
} else {
|
||||
user.username = telegramUser.username || telegramUser.first_name;
|
||||
user.firstName = telegramUser.first_name;
|
||||
user.lastName = telegramUser.last_name;
|
||||
// Обновлять только если есть новые данные, не перезаписывать существующие пустыми значениями
|
||||
if (telegramUser.username) {
|
||||
user.username = telegramUser.username;
|
||||
} else if (!user.username && telegramUser.first_name) {
|
||||
// Если username пустой, использовать first_name как fallback
|
||||
user.username = telegramUser.first_name;
|
||||
}
|
||||
|
||||
if (telegramUser.first_name) {
|
||||
user.firstName = telegramUser.first_name;
|
||||
}
|
||||
|
||||
if (telegramUser.last_name !== undefined) {
|
||||
user.lastName = telegramUser.last_name || '';
|
||||
}
|
||||
|
||||
// Обновлять аватарку только если есть новая
|
||||
if (telegramUser.photo_url) {
|
||||
user.photoUrl = telegramUser.photo_url;
|
||||
}
|
||||
|
||||
await user.save();
|
||||
}
|
||||
|
||||
|
|
@ -188,19 +259,34 @@ const authenticateModeration = async (req, res, next) => {
|
|||
if (!user) {
|
||||
user = new User({
|
||||
telegramId: telegramUser.id.toString(),
|
||||
username: telegramUser.username || telegramUser.first_name,
|
||||
firstName: telegramUser.first_name,
|
||||
lastName: telegramUser.last_name,
|
||||
photoUrl: telegramUser.photo_url
|
||||
username: telegramUser.username || telegramUser.first_name || 'user',
|
||||
firstName: telegramUser.first_name || '',
|
||||
lastName: telegramUser.last_name || '',
|
||||
photoUrl: telegramUser.photo_url || null
|
||||
});
|
||||
await user.save();
|
||||
} else {
|
||||
user.username = telegramUser.username || telegramUser.first_name;
|
||||
user.firstName = telegramUser.first_name;
|
||||
user.lastName = telegramUser.last_name;
|
||||
// Обновлять только если есть новые данные, не перезаписывать существующие пустыми значениями
|
||||
if (telegramUser.username) {
|
||||
user.username = telegramUser.username;
|
||||
} else if (!user.username && telegramUser.first_name) {
|
||||
// Если username пустой, использовать first_name как fallback
|
||||
user.username = telegramUser.first_name;
|
||||
}
|
||||
|
||||
if (telegramUser.first_name) {
|
||||
user.firstName = telegramUser.first_name;
|
||||
}
|
||||
|
||||
if (telegramUser.last_name !== undefined) {
|
||||
user.lastName = telegramUser.last_name || '';
|
||||
}
|
||||
|
||||
// Обновлять аватарку только если есть новая
|
||||
if (telegramUser.photo_url) {
|
||||
user.photoUrl = telegramUser.photo_url;
|
||||
}
|
||||
|
||||
await user.save();
|
||||
}
|
||||
|
||||
|
|
@ -208,6 +294,8 @@ const authenticateModeration = async (req, res, next) => {
|
|||
return res.status(403).json({ error: 'Пользователь заблокирован' });
|
||||
}
|
||||
|
||||
// Подтянуть отсутствующие данные из Telegram
|
||||
await ensureUserData(user, telegramUser);
|
||||
await ensureUserSettings(user);
|
||||
await touchUserActivity(user);
|
||||
|
||||
|
|
@ -226,5 +314,6 @@ module.exports = {
|
|||
requireModerator,
|
||||
requireAdmin,
|
||||
touchUserActivity,
|
||||
ensureUserSettings
|
||||
ensureUserSettings,
|
||||
ensureUserData
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ const config = require('../config');
|
|||
const { validateTelegramId } = require('../middleware/validator');
|
||||
const { logSecurityEvent } = require('../middleware/logger');
|
||||
const { strictAuthLimiter } = require('../middleware/security');
|
||||
const { authenticate, ensureUserSettings, touchUserActivity } = require('../middleware/auth');
|
||||
const { authenticate, ensureUserSettings, touchUserActivity, ensureUserData } = require('../middleware/auth');
|
||||
const { fetchLatestAvatar } = require('../jobs/avatarUpdater');
|
||||
|
||||
const ALLOWED_SEARCH_PREFERENCES = ['furry', 'anime'];
|
||||
|
||||
|
|
@ -174,22 +175,41 @@ router.post('/oauth', strictAuthLimiter, async (req, res) => {
|
|||
if (!user) {
|
||||
user = new User({
|
||||
telegramId: telegramUser.id.toString(),
|
||||
username: telegramUser.username || telegramUser.first_name,
|
||||
firstName: telegramUser.first_name,
|
||||
lastName: telegramUser.last_name,
|
||||
photoUrl: telegramUser.photo_url
|
||||
username: telegramUser.username || telegramUser.first_name || 'user',
|
||||
firstName: telegramUser.first_name || '',
|
||||
lastName: telegramUser.last_name || '',
|
||||
photoUrl: telegramUser.photo_url || null
|
||||
});
|
||||
await user.save();
|
||||
console.log(`✅ Создан новый пользователь через OAuth: ${user.username}`);
|
||||
} else {
|
||||
// Обновить данные пользователя
|
||||
user.username = telegramUser.username || telegramUser.first_name;
|
||||
user.firstName = telegramUser.first_name;
|
||||
user.lastName = telegramUser.last_name;
|
||||
user.photoUrl = telegramUser.photo_url;
|
||||
// Обновлять только если есть новые данные, не перезаписывать существующие пустыми значениями
|
||||
if (telegramUser.username) {
|
||||
user.username = telegramUser.username;
|
||||
} else if (!user.username && telegramUser.first_name) {
|
||||
// Если username пустой, использовать first_name как fallback
|
||||
user.username = telegramUser.first_name;
|
||||
}
|
||||
|
||||
if (telegramUser.first_name) {
|
||||
user.firstName = telegramUser.first_name;
|
||||
}
|
||||
|
||||
if (telegramUser.last_name !== undefined) {
|
||||
user.lastName = telegramUser.last_name || '';
|
||||
}
|
||||
|
||||
// Обновлять аватарку только если есть новая
|
||||
if (telegramUser.photo_url) {
|
||||
user.photoUrl = telegramUser.photo_url;
|
||||
}
|
||||
|
||||
await user.save();
|
||||
}
|
||||
|
||||
// Подтянуть отсутствующие данные из Telegram
|
||||
await ensureUserData(user, telegramUser);
|
||||
|
||||
// Получить полные данные пользователя
|
||||
const populatedUser = await User.findById(user._id).populate([
|
||||
{ path: 'followers', select: 'username firstName lastName photoUrl' },
|
||||
|
|
|
|||
|
|
@ -259,7 +259,8 @@ app.use(errorHandler);
|
|||
|
||||
// Инициализировать WebSocket
|
||||
initWebSocket(server);
|
||||
scheduleAvatarUpdates();
|
||||
// Автообновление аватарок отключено - обновление происходит только при перезаходе
|
||||
// scheduleAvatarUpdates();
|
||||
startServerMonitorBot();
|
||||
|
||||
// Обработка необработанных ошибок
|
||||
|
|
|
|||
|
|
@ -0,0 +1,167 @@
|
|||
# Диагностика: Посты не сохраняются
|
||||
|
||||
## 🔴 Проблема
|
||||
Посты создаются в интерфейсе, но исчезают при обновлении страницы.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Шаг 1: Проверьте логи backend
|
||||
|
||||
```bash
|
||||
# Посмотрите логи backend
|
||||
docker logs nakama-backend -f
|
||||
|
||||
# Или только последние 100 строк
|
||||
docker logs nakama-backend --tail 100
|
||||
```
|
||||
|
||||
**Что искать:**
|
||||
- ❌ `Ошибка создания поста`
|
||||
- ❌ `S3 клиент не инициализирован`
|
||||
- ❌ `Ошибка загрузки в MinIO`
|
||||
- ❌ `403` или `Access Denied`
|
||||
- ✅ `Файлы загружены в MinIO`
|
||||
- ✅ `POST /api/posts 201`
|
||||
|
||||
---
|
||||
|
||||
## ✅ Шаг 2: Проверьте MinIO bucket
|
||||
|
||||
### Вариант А: Через консоль браузера
|
||||
|
||||
1. Откройте DevTools (F12) в браузере
|
||||
2. Вкладка **Network**
|
||||
3. Попробуйте создать пост
|
||||
4. Найдите запрос `POST /api/posts`
|
||||
5. Посмотрите на:
|
||||
- **Status**: должен быть `201 Created`
|
||||
- **Response**: должен содержать объект `post` с `_id`
|
||||
- **Если 500**: смотрите `error` в ответе
|
||||
|
||||
### Вариант Б: Проверьте bucket в MinIO Console
|
||||
|
||||
1. Откройте http://103.80.87.247:9901/
|
||||
2. **Buckets** → **nakama-media** → **posts/**
|
||||
3. Должны видеть загруженные файлы
|
||||
|
||||
---
|
||||
|
||||
## ✅ Шаг 3: Убедитесь, что bucket публичный
|
||||
|
||||
```bash
|
||||
# На сервере с MinIO
|
||||
mc alias set myminio http://localhost:9000 minioadmin minioadmin
|
||||
mc anonymous get myminio/nakama-media
|
||||
|
||||
# Должно быть: Access permission for 'myminio/nakama-media' is 'download'
|
||||
# Если нет, выполните:
|
||||
mc anonymous set download myminio/nakama-media
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Шаг 4: Проверьте .env
|
||||
|
||||
Откройте `.env` (в корне проекта) и убедитесь:
|
||||
|
||||
```env
|
||||
# MinIO ДОЛЖЕН быть включен
|
||||
MINIO_ENABLED=true
|
||||
|
||||
# Правильные настройки
|
||||
MINIO_ENDPOINT=103.80.87.247
|
||||
MINIO_PORT=9000
|
||||
MINIO_USE_SSL=false
|
||||
MINIO_ACCESS_KEY=minioadmin
|
||||
MINIO_SECRET_KEY=minioadmin
|
||||
MINIO_BUCKET=nakama-media
|
||||
MINIO_PUBLIC_BUCKET=true
|
||||
|
||||
# База данных
|
||||
MONGODB_URI=mongodb://103.80.87.247:27017/nakama
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Шаг 5: Перезапустите backend
|
||||
|
||||
```bash
|
||||
docker compose restart backend
|
||||
|
||||
# Посмотрите логи запуска
|
||||
docker logs nakama-backend --tail 50
|
||||
```
|
||||
|
||||
**Что должно быть в логах:**
|
||||
```
|
||||
✅ [SUCCESS] MinIO успешно подключен
|
||||
📝 [INFO] S3 клиент для MinIO инициализирован
|
||||
📝 [INFO] Bucket nakama-media существует
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Шаг 6: Тестовый запрос
|
||||
|
||||
```bash
|
||||
# Создайте тестовый пост через curl
|
||||
curl -X POST http://your-backend-url/api/posts \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-F "content=Test post" \
|
||||
-F "tags=[\"furry\"]" \
|
||||
-F "isNSFW=false"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Если все еще не работает
|
||||
|
||||
### Проверьте подключение к MongoDB:
|
||||
|
||||
```bash
|
||||
# На сервере с MongoDB
|
||||
docker exec -it nakama-mongodb mongosh
|
||||
|
||||
# В консоли MongoDB
|
||||
use nakama
|
||||
db.posts.find().limit(5)
|
||||
```
|
||||
|
||||
Если посты есть в БД, но не отображаются в интерфейсе - проблема в frontend или API запросе.
|
||||
|
||||
Если постов нет - проблема в backend при сохранении.
|
||||
|
||||
---
|
||||
|
||||
## 📋 Контрольный список
|
||||
|
||||
- [ ] Логи backend не содержат ошибок
|
||||
- [ ] MinIO bucket `nakama-media` существует
|
||||
- [ ] Bucket публичный (anonymous download)
|
||||
- [ ] `.env` настроен правильно (`MINIO_ENABLED=true`)
|
||||
- [ ] Backend перезапущен
|
||||
- [ ] MongoDB доступна (`mongodb://103.80.87.247:27017/nakama`)
|
||||
- [ ] В консоли браузера нет ошибок при создании поста
|
||||
|
||||
---
|
||||
|
||||
## 💡 Быстрое решение
|
||||
|
||||
```bash
|
||||
# 1. Сделайте bucket публичным
|
||||
mc alias set myminio http://103.80.87.247:9000 minioadmin minioadmin
|
||||
mc anonymous set download myminio/nakama-media
|
||||
|
||||
# 2. Проверьте .env
|
||||
grep MINIO .env
|
||||
|
||||
# 3. Перезапустите
|
||||
docker compose restart backend
|
||||
|
||||
# 4. Проверьте логи
|
||||
docker logs nakama-backend -f
|
||||
```
|
||||
|
||||
Теперь попробуйте создать пост!
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Скрипт для настройки публичного доступа к MinIO bucket
|
||||
# Использование: bash fix-minio-public.sh
|
||||
|
||||
MINIO_ENDPOINT="http://103.80.87.247:9000"
|
||||
MINIO_ACCESS_KEY="minioadmin"
|
||||
MINIO_SECRET_KEY="minioadmin"
|
||||
BUCKET_NAME="nakama-media"
|
||||
|
||||
echo "🔧 Настройка публичного доступа к MinIO bucket..."
|
||||
|
||||
# Проверка наличия mc
|
||||
if ! command -v mc &> /dev/null; then
|
||||
echo "📥 Устанавливаю MinIO Client (mc)..."
|
||||
curl -s -O https://dl.min.io/client/mc/release/linux-amd64/mc
|
||||
chmod +x mc
|
||||
sudo mv mc /usr/local/bin/
|
||||
echo "✅ MinIO Client установлен"
|
||||
fi
|
||||
|
||||
# Настройка alias
|
||||
echo "🔗 Подключаюсь к MinIO..."
|
||||
mc alias set myminio $MINIO_ENDPOINT $MINIO_ACCESS_KEY $MINIO_SECRET_KEY
|
||||
|
||||
# Проверка существования bucket
|
||||
echo "📦 Проверяю bucket $BUCKET_NAME..."
|
||||
if ! mc ls myminio/$BUCKET_NAME &> /dev/null; then
|
||||
echo "❌ Bucket $BUCKET_NAME не найден!"
|
||||
echo "Создаю bucket..."
|
||||
mc mb myminio/$BUCKET_NAME
|
||||
fi
|
||||
|
||||
# Установка публичной политики
|
||||
echo "🔓 Делаю bucket публичным для чтения..."
|
||||
mc anonymous set download myminio/$BUCKET_NAME
|
||||
|
||||
# Проверка политики
|
||||
echo "✅ Текущая политика:"
|
||||
mc anonymous get myminio/$BUCKET_NAME
|
||||
|
||||
echo ""
|
||||
echo "🎉 Готово! Теперь файлы в bucket $BUCKET_NAME доступны публично"
|
||||
echo ""
|
||||
echo "📝 Не забудьте добавить в .env:"
|
||||
echo "MINIO_PUBLIC_BUCKET=true"
|
||||
echo "MINIO_ENDPOINT=103.80.87.247"
|
||||
echo "MINIO_PORT=9000"
|
||||
echo "MINIO_USE_SSL=false"
|
||||
echo ""
|
||||
echo "🔄 После изменений перезапустите backend:"
|
||||
echo "docker compose restart backend"
|
||||
|
||||
|
||||
|
|
@ -65,14 +65,15 @@ export default function CommentsModal({ post, onClose, onUpdate }) {
|
|||
<div className="preview-author">
|
||||
<img
|
||||
src={post.author.photoUrl || '/default-avatar.png'}
|
||||
alt={post.author.username}
|
||||
alt={post.author.username || post.author.firstName || 'User'}
|
||||
className="preview-avatar"
|
||||
/>
|
||||
<div>
|
||||
<div className="preview-name">
|
||||
{post.author.firstName} {post.author.lastName}
|
||||
{post.author.firstName || ''} {post.author.lastName || ''}
|
||||
{!post.author.firstName && !post.author.lastName && 'Пользователь'}
|
||||
</div>
|
||||
<div className="preview-username">@{post.author.username}</div>
|
||||
<div className="preview-username">@{post.author.username || post.author.firstName || 'user'}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -99,13 +100,14 @@ export default function CommentsModal({ post, onClose, onUpdate }) {
|
|||
<div key={index} className="comment-item fade-in">
|
||||
<img
|
||||
src={c.author.photoUrl || '/default-avatar.png'}
|
||||
alt={c.author.username}
|
||||
alt={c.author.username || c.author.firstName || 'User'}
|
||||
className="comment-avatar"
|
||||
/>
|
||||
<div className="comment-content">
|
||||
<div className="comment-header">
|
||||
<span className="comment-author">
|
||||
{c.author.firstName} {c.author.lastName}
|
||||
{c.author.firstName || ''} {c.author.lastName || ''}
|
||||
{!c.author.firstName && !c.author.lastName && 'Пользователь'}
|
||||
</span>
|
||||
<span className="comment-time">{formatDate(c.createdAt)}</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -104,15 +104,16 @@ export default function PostCard({ post, currentUser, onUpdate }) {
|
|||
<div className="post-author" onClick={goToProfile}>
|
||||
<img
|
||||
src={post.author.photoUrl || '/default-avatar.png'}
|
||||
alt={post.author.username}
|
||||
alt={post.author.username || post.author.firstName || 'User'}
|
||||
className="author-avatar"
|
||||
/>
|
||||
<div className="author-info">
|
||||
<div className="author-name">
|
||||
{post.author.firstName} {post.author.lastName}
|
||||
{post.author.firstName || ''} {post.author.lastName || ''}
|
||||
{!post.author.firstName && !post.author.lastName && 'Пользователь'}
|
||||
</div>
|
||||
<div className="post-date">
|
||||
@{post.author.username} · {formatDate(post.createdAt)}
|
||||
@{post.author.username || post.author.firstName || 'user'} · {formatDate(post.createdAt)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -123,14 +123,15 @@ export default function CommentsPage({ user }) {
|
|||
<div className="preview-author">
|
||||
<img
|
||||
src={post.author.photoUrl || '/default-avatar.png'}
|
||||
alt={post.author.username}
|
||||
alt={post.author.username || post.author.firstName || 'User'}
|
||||
className="preview-avatar"
|
||||
/>
|
||||
<div>
|
||||
<div className="preview-name">
|
||||
{post.author.firstName} {post.author.lastName}
|
||||
{post.author.firstName || ''} {post.author.lastName || ''}
|
||||
{!post.author.firstName && !post.author.lastName && 'Пользователь'}
|
||||
</div>
|
||||
<div className="preview-username">@{post.author.username}</div>
|
||||
<div className="preview-username">@{post.author.username || post.author.firstName || 'user'}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -164,13 +165,14 @@ export default function CommentsPage({ user }) {
|
|||
<div key={index} className="comment-item fade-in">
|
||||
<img
|
||||
src={c.author.photoUrl || '/default-avatar.png'}
|
||||
alt={c.author.username}
|
||||
alt={c.author.username || c.author.firstName || 'User'}
|
||||
className="comment-avatar"
|
||||
/>
|
||||
<div className="comment-content">
|
||||
<div className="comment-header">
|
||||
<span className="comment-author">
|
||||
{c.author.firstName} {c.author.lastName}
|
||||
{c.author.firstName || ''} {c.author.lastName || ''}
|
||||
{!c.author.firstName && !c.author.lastName && 'Пользователь'}
|
||||
</span>
|
||||
<span className="comment-time">
|
||||
{formatDate(c.createdAt)}
|
||||
|
|
|
|||
|
|
@ -189,14 +189,15 @@ export default function PostMenuPage({ user }) {
|
|||
<div className="preview-author">
|
||||
<img
|
||||
src={post.author.photoUrl || '/default-avatar.png'}
|
||||
alt={post.author.username}
|
||||
alt={post.author.username || post.author.firstName || 'User'}
|
||||
className="preview-avatar"
|
||||
/>
|
||||
<div>
|
||||
<div className="preview-name">
|
||||
{post.author.firstName} {post.author.lastName}
|
||||
{post.author.firstName || ''} {post.author.lastName || ''}
|
||||
{!post.author.firstName && !post.author.lastName && 'Пользователь'}
|
||||
</div>
|
||||
<div className="preview-username">@{post.author.username}</div>
|
||||
<div className="preview-username">@{post.author.username || post.author.firstName || 'user'}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -122,18 +122,19 @@ export default function Profile({ user, setUser }) {
|
|||
<div className="profile-info card">
|
||||
<img
|
||||
src={user.photoUrl || '/default-avatar.png'}
|
||||
alt={user.username}
|
||||
alt={user.username || user.firstName || 'User'}
|
||||
className="profile-avatar"
|
||||
/>
|
||||
|
||||
<div className="profile-details">
|
||||
<h2 className="profile-name">
|
||||
{user.firstName} {user.lastName}
|
||||
{user.firstName || ''} {user.lastName || ''}
|
||||
{!user.firstName && !user.lastName && 'Пользователь'}
|
||||
{(user.role === 'moderator' || user.role === 'admin') && (
|
||||
<Shield size={20} color="var(--button-accent)" />
|
||||
)}
|
||||
</h2>
|
||||
<p className="profile-username">@{user.username}</p>
|
||||
<p className="profile-username">@{user.username || user.firstName || 'user'}</p>
|
||||
|
||||
{user.bio ? (
|
||||
<div className="profile-bio">
|
||||
|
|
|
|||
|
|
@ -86,18 +86,19 @@ export default function UserProfile({ currentUser }) {
|
|||
<div className="user-info card">
|
||||
<img
|
||||
src={user.photoUrl || '/default-avatar.png'}
|
||||
alt={user.username}
|
||||
alt={user.username || user.firstName || 'User'}
|
||||
className="user-avatar"
|
||||
/>
|
||||
|
||||
<div className="user-details">
|
||||
<h2 className="user-name">
|
||||
{user.firstName} {user.lastName}
|
||||
{user.firstName || ''} {user.lastName || ''}
|
||||
{!user.firstName && !user.lastName && 'Пользователь'}
|
||||
{(user.role === 'moderator' || user.role === 'admin') && (
|
||||
<Shield size={20} color="var(--button-accent)" />
|
||||
)}
|
||||
</h2>
|
||||
<p className="user-username">@{user.username}</p>
|
||||
<p className="user-username">@{user.username || user.firstName || 'user'}</p>
|
||||
|
||||
{user.bio && (
|
||||
<div className="user-bio">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Principal": {
|
||||
"AWS": ["*"]
|
||||
},
|
||||
"Action": [
|
||||
"s3:GetObject"
|
||||
],
|
||||
"Resource": [
|
||||
"arn:aws:s3:::nakama-media/*"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue