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 = {
|
module.exports = {
|
||||||
scheduleAvatarUpdates,
|
scheduleAvatarUpdates,
|
||||||
updateAllUserAvatars
|
updateAllUserAvatars,
|
||||||
|
fetchLatestAvatar
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ const User = require('../models/User');
|
||||||
const { validateTelegramId } = require('./validator');
|
const { validateTelegramId } = require('./validator');
|
||||||
const { logSecurityEvent } = require('./logger');
|
const { logSecurityEvent } = require('./logger');
|
||||||
const { validateAndParseInitData } = require('../utils/telegram');
|
const { validateAndParseInitData } = require('../utils/telegram');
|
||||||
|
const { fetchLatestAvatar } = require('../jobs/avatarUpdater');
|
||||||
|
|
||||||
const OFFICIAL_CLIENT_MESSAGE = 'Используйте официальный клиент. Сообщите об ошибке в https://t.me/NakamaReportbot';
|
const OFFICIAL_CLIENT_MESSAGE = 'Используйте официальный клиент. Сообщите об ошибке в https://t.me/NakamaReportbot';
|
||||||
const ALLOWED_SEARCH_PREFERENCES = ['furry', 'anime'];
|
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) => {
|
const authenticate = async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const authHeader = req.headers.authorization || '';
|
const authHeader = req.headers.authorization || '';
|
||||||
|
|
@ -94,19 +150,34 @@ const authenticate = async (req, res, next) => {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
user = new User({
|
user = new User({
|
||||||
telegramId: telegramUser.id.toString(),
|
telegramId: telegramUser.id.toString(),
|
||||||
username: telegramUser.username || telegramUser.first_name,
|
username: telegramUser.username || telegramUser.first_name || 'user',
|
||||||
firstName: telegramUser.first_name,
|
firstName: telegramUser.first_name || '',
|
||||||
lastName: telegramUser.last_name,
|
lastName: telegramUser.last_name || '',
|
||||||
photoUrl: telegramUser.photo_url
|
photoUrl: telegramUser.photo_url || null
|
||||||
});
|
});
|
||||||
await user.save();
|
await user.save();
|
||||||
} else {
|
} else {
|
||||||
user.username = telegramUser.username || telegramUser.first_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;
|
user.firstName = telegramUser.first_name;
|
||||||
user.lastName = telegramUser.last_name;
|
}
|
||||||
|
|
||||||
|
if (telegramUser.last_name !== undefined) {
|
||||||
|
user.lastName = telegramUser.last_name || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновлять аватарку только если есть новая
|
||||||
if (telegramUser.photo_url) {
|
if (telegramUser.photo_url) {
|
||||||
user.photoUrl = telegramUser.photo_url;
|
user.photoUrl = telegramUser.photo_url;
|
||||||
}
|
}
|
||||||
|
|
||||||
await user.save();
|
await user.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -188,19 +259,34 @@ const authenticateModeration = async (req, res, next) => {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
user = new User({
|
user = new User({
|
||||||
telegramId: telegramUser.id.toString(),
|
telegramId: telegramUser.id.toString(),
|
||||||
username: telegramUser.username || telegramUser.first_name,
|
username: telegramUser.username || telegramUser.first_name || 'user',
|
||||||
firstName: telegramUser.first_name,
|
firstName: telegramUser.first_name || '',
|
||||||
lastName: telegramUser.last_name,
|
lastName: telegramUser.last_name || '',
|
||||||
photoUrl: telegramUser.photo_url
|
photoUrl: telegramUser.photo_url || null
|
||||||
});
|
});
|
||||||
await user.save();
|
await user.save();
|
||||||
} else {
|
} else {
|
||||||
user.username = telegramUser.username || telegramUser.first_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;
|
user.firstName = telegramUser.first_name;
|
||||||
user.lastName = telegramUser.last_name;
|
}
|
||||||
|
|
||||||
|
if (telegramUser.last_name !== undefined) {
|
||||||
|
user.lastName = telegramUser.last_name || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновлять аватарку только если есть новая
|
||||||
if (telegramUser.photo_url) {
|
if (telegramUser.photo_url) {
|
||||||
user.photoUrl = telegramUser.photo_url;
|
user.photoUrl = telegramUser.photo_url;
|
||||||
}
|
}
|
||||||
|
|
||||||
await user.save();
|
await user.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -208,6 +294,8 @@ const authenticateModeration = async (req, res, next) => {
|
||||||
return res.status(403).json({ error: 'Пользователь заблокирован' });
|
return res.status(403).json({ error: 'Пользователь заблокирован' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Подтянуть отсутствующие данные из Telegram
|
||||||
|
await ensureUserData(user, telegramUser);
|
||||||
await ensureUserSettings(user);
|
await ensureUserSettings(user);
|
||||||
await touchUserActivity(user);
|
await touchUserActivity(user);
|
||||||
|
|
||||||
|
|
@ -226,5 +314,6 @@ module.exports = {
|
||||||
requireModerator,
|
requireModerator,
|
||||||
requireAdmin,
|
requireAdmin,
|
||||||
touchUserActivity,
|
touchUserActivity,
|
||||||
ensureUserSettings
|
ensureUserSettings,
|
||||||
|
ensureUserData
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,8 @@ const config = require('../config');
|
||||||
const { validateTelegramId } = require('../middleware/validator');
|
const { validateTelegramId } = require('../middleware/validator');
|
||||||
const { logSecurityEvent } = require('../middleware/logger');
|
const { logSecurityEvent } = require('../middleware/logger');
|
||||||
const { strictAuthLimiter } = require('../middleware/security');
|
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'];
|
const ALLOWED_SEARCH_PREFERENCES = ['furry', 'anime'];
|
||||||
|
|
||||||
|
|
@ -174,22 +175,41 @@ router.post('/oauth', strictAuthLimiter, async (req, res) => {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
user = new User({
|
user = new User({
|
||||||
telegramId: telegramUser.id.toString(),
|
telegramId: telegramUser.id.toString(),
|
||||||
username: telegramUser.username || telegramUser.first_name,
|
username: telegramUser.username || telegramUser.first_name || 'user',
|
||||||
firstName: telegramUser.first_name,
|
firstName: telegramUser.first_name || '',
|
||||||
lastName: telegramUser.last_name,
|
lastName: telegramUser.last_name || '',
|
||||||
photoUrl: telegramUser.photo_url
|
photoUrl: telegramUser.photo_url || null
|
||||||
});
|
});
|
||||||
await user.save();
|
await user.save();
|
||||||
console.log(`✅ Создан новый пользователь через OAuth: ${user.username}`);
|
console.log(`✅ Создан новый пользователь через OAuth: ${user.username}`);
|
||||||
} else {
|
} else {
|
||||||
// Обновить данные пользователя
|
// Обновлять только если есть новые данные, не перезаписывать существующие пустыми значениями
|
||||||
user.username = telegramUser.username || telegramUser.first_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;
|
user.firstName = telegramUser.first_name;
|
||||||
user.lastName = telegramUser.last_name;
|
}
|
||||||
|
|
||||||
|
if (telegramUser.last_name !== undefined) {
|
||||||
|
user.lastName = telegramUser.last_name || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновлять аватарку только если есть новая
|
||||||
|
if (telegramUser.photo_url) {
|
||||||
user.photoUrl = telegramUser.photo_url;
|
user.photoUrl = telegramUser.photo_url;
|
||||||
|
}
|
||||||
|
|
||||||
await user.save();
|
await user.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Подтянуть отсутствующие данные из Telegram
|
||||||
|
await ensureUserData(user, telegramUser);
|
||||||
|
|
||||||
// Получить полные данные пользователя
|
// Получить полные данные пользователя
|
||||||
const populatedUser = await User.findById(user._id).populate([
|
const populatedUser = await User.findById(user._id).populate([
|
||||||
{ path: 'followers', select: 'username firstName lastName photoUrl' },
|
{ path: 'followers', select: 'username firstName lastName photoUrl' },
|
||||||
|
|
|
||||||
|
|
@ -259,7 +259,8 @@ app.use(errorHandler);
|
||||||
|
|
||||||
// Инициализировать WebSocket
|
// Инициализировать WebSocket
|
||||||
initWebSocket(server);
|
initWebSocket(server);
|
||||||
scheduleAvatarUpdates();
|
// Автообновление аватарок отключено - обновление происходит только при перезаходе
|
||||||
|
// scheduleAvatarUpdates();
|
||||||
startServerMonitorBot();
|
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">
|
<div className="preview-author">
|
||||||
<img
|
<img
|
||||||
src={post.author.photoUrl || '/default-avatar.png'}
|
src={post.author.photoUrl || '/default-avatar.png'}
|
||||||
alt={post.author.username}
|
alt={post.author.username || post.author.firstName || 'User'}
|
||||||
className="preview-avatar"
|
className="preview-avatar"
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<div className="preview-name">
|
<div className="preview-name">
|
||||||
{post.author.firstName} {post.author.lastName}
|
{post.author.firstName || ''} {post.author.lastName || ''}
|
||||||
|
{!post.author.firstName && !post.author.lastName && 'Пользователь'}
|
||||||
</div>
|
</div>
|
||||||
<div className="preview-username">@{post.author.username}</div>
|
<div className="preview-username">@{post.author.username || post.author.firstName || 'user'}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -99,13 +100,14 @@ export default function CommentsModal({ post, onClose, onUpdate }) {
|
||||||
<div key={index} className="comment-item fade-in">
|
<div key={index} className="comment-item fade-in">
|
||||||
<img
|
<img
|
||||||
src={c.author.photoUrl || '/default-avatar.png'}
|
src={c.author.photoUrl || '/default-avatar.png'}
|
||||||
alt={c.author.username}
|
alt={c.author.username || c.author.firstName || 'User'}
|
||||||
className="comment-avatar"
|
className="comment-avatar"
|
||||||
/>
|
/>
|
||||||
<div className="comment-content">
|
<div className="comment-content">
|
||||||
<div className="comment-header">
|
<div className="comment-header">
|
||||||
<span className="comment-author">
|
<span className="comment-author">
|
||||||
{c.author.firstName} {c.author.lastName}
|
{c.author.firstName || ''} {c.author.lastName || ''}
|
||||||
|
{!c.author.firstName && !c.author.lastName && 'Пользователь'}
|
||||||
</span>
|
</span>
|
||||||
<span className="comment-time">{formatDate(c.createdAt)}</span>
|
<span className="comment-time">{formatDate(c.createdAt)}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -104,15 +104,16 @@ export default function PostCard({ post, currentUser, onUpdate }) {
|
||||||
<div className="post-author" onClick={goToProfile}>
|
<div className="post-author" onClick={goToProfile}>
|
||||||
<img
|
<img
|
||||||
src={post.author.photoUrl || '/default-avatar.png'}
|
src={post.author.photoUrl || '/default-avatar.png'}
|
||||||
alt={post.author.username}
|
alt={post.author.username || post.author.firstName || 'User'}
|
||||||
className="author-avatar"
|
className="author-avatar"
|
||||||
/>
|
/>
|
||||||
<div className="author-info">
|
<div className="author-info">
|
||||||
<div className="author-name">
|
<div className="author-name">
|
||||||
{post.author.firstName} {post.author.lastName}
|
{post.author.firstName || ''} {post.author.lastName || ''}
|
||||||
|
{!post.author.firstName && !post.author.lastName && 'Пользователь'}
|
||||||
</div>
|
</div>
|
||||||
<div className="post-date">
|
<div className="post-date">
|
||||||
@{post.author.username} · {formatDate(post.createdAt)}
|
@{post.author.username || post.author.firstName || 'user'} · {formatDate(post.createdAt)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -123,14 +123,15 @@ export default function CommentsPage({ user }) {
|
||||||
<div className="preview-author">
|
<div className="preview-author">
|
||||||
<img
|
<img
|
||||||
src={post.author.photoUrl || '/default-avatar.png'}
|
src={post.author.photoUrl || '/default-avatar.png'}
|
||||||
alt={post.author.username}
|
alt={post.author.username || post.author.firstName || 'User'}
|
||||||
className="preview-avatar"
|
className="preview-avatar"
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<div className="preview-name">
|
<div className="preview-name">
|
||||||
{post.author.firstName} {post.author.lastName}
|
{post.author.firstName || ''} {post.author.lastName || ''}
|
||||||
|
{!post.author.firstName && !post.author.lastName && 'Пользователь'}
|
||||||
</div>
|
</div>
|
||||||
<div className="preview-username">@{post.author.username}</div>
|
<div className="preview-username">@{post.author.username || post.author.firstName || 'user'}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -164,13 +165,14 @@ export default function CommentsPage({ user }) {
|
||||||
<div key={index} className="comment-item fade-in">
|
<div key={index} className="comment-item fade-in">
|
||||||
<img
|
<img
|
||||||
src={c.author.photoUrl || '/default-avatar.png'}
|
src={c.author.photoUrl || '/default-avatar.png'}
|
||||||
alt={c.author.username}
|
alt={c.author.username || c.author.firstName || 'User'}
|
||||||
className="comment-avatar"
|
className="comment-avatar"
|
||||||
/>
|
/>
|
||||||
<div className="comment-content">
|
<div className="comment-content">
|
||||||
<div className="comment-header">
|
<div className="comment-header">
|
||||||
<span className="comment-author">
|
<span className="comment-author">
|
||||||
{c.author.firstName} {c.author.lastName}
|
{c.author.firstName || ''} {c.author.lastName || ''}
|
||||||
|
{!c.author.firstName && !c.author.lastName && 'Пользователь'}
|
||||||
</span>
|
</span>
|
||||||
<span className="comment-time">
|
<span className="comment-time">
|
||||||
{formatDate(c.createdAt)}
|
{formatDate(c.createdAt)}
|
||||||
|
|
|
||||||
|
|
@ -189,14 +189,15 @@ export default function PostMenuPage({ user }) {
|
||||||
<div className="preview-author">
|
<div className="preview-author">
|
||||||
<img
|
<img
|
||||||
src={post.author.photoUrl || '/default-avatar.png'}
|
src={post.author.photoUrl || '/default-avatar.png'}
|
||||||
alt={post.author.username}
|
alt={post.author.username || post.author.firstName || 'User'}
|
||||||
className="preview-avatar"
|
className="preview-avatar"
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<div className="preview-name">
|
<div className="preview-name">
|
||||||
{post.author.firstName} {post.author.lastName}
|
{post.author.firstName || ''} {post.author.lastName || ''}
|
||||||
|
{!post.author.firstName && !post.author.lastName && 'Пользователь'}
|
||||||
</div>
|
</div>
|
||||||
<div className="preview-username">@{post.author.username}</div>
|
<div className="preview-username">@{post.author.username || post.author.firstName || 'user'}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -122,18 +122,19 @@ export default function Profile({ user, setUser }) {
|
||||||
<div className="profile-info card">
|
<div className="profile-info card">
|
||||||
<img
|
<img
|
||||||
src={user.photoUrl || '/default-avatar.png'}
|
src={user.photoUrl || '/default-avatar.png'}
|
||||||
alt={user.username}
|
alt={user.username || user.firstName || 'User'}
|
||||||
className="profile-avatar"
|
className="profile-avatar"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="profile-details">
|
<div className="profile-details">
|
||||||
<h2 className="profile-name">
|
<h2 className="profile-name">
|
||||||
{user.firstName} {user.lastName}
|
{user.firstName || ''} {user.lastName || ''}
|
||||||
|
{!user.firstName && !user.lastName && 'Пользователь'}
|
||||||
{(user.role === 'moderator' || user.role === 'admin') && (
|
{(user.role === 'moderator' || user.role === 'admin') && (
|
||||||
<Shield size={20} color="var(--button-accent)" />
|
<Shield size={20} color="var(--button-accent)" />
|
||||||
)}
|
)}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="profile-username">@{user.username}</p>
|
<p className="profile-username">@{user.username || user.firstName || 'user'}</p>
|
||||||
|
|
||||||
{user.bio ? (
|
{user.bio ? (
|
||||||
<div className="profile-bio">
|
<div className="profile-bio">
|
||||||
|
|
|
||||||
|
|
@ -86,18 +86,19 @@ export default function UserProfile({ currentUser }) {
|
||||||
<div className="user-info card">
|
<div className="user-info card">
|
||||||
<img
|
<img
|
||||||
src={user.photoUrl || '/default-avatar.png'}
|
src={user.photoUrl || '/default-avatar.png'}
|
||||||
alt={user.username}
|
alt={user.username || user.firstName || 'User'}
|
||||||
className="user-avatar"
|
className="user-avatar"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="user-details">
|
<div className="user-details">
|
||||||
<h2 className="user-name">
|
<h2 className="user-name">
|
||||||
{user.firstName} {user.lastName}
|
{user.firstName || ''} {user.lastName || ''}
|
||||||
|
{!user.firstName && !user.lastName && 'Пользователь'}
|
||||||
{(user.role === 'moderator' || user.role === 'admin') && (
|
{(user.role === 'moderator' || user.role === 'admin') && (
|
||||||
<Shield size={20} color="var(--button-accent)" />
|
<Shield size={20} color="var(--button-accent)" />
|
||||||
)}
|
)}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="user-username">@{user.username}</p>
|
<p className="user-username">@{user.username || user.firstName || 'user'}</p>
|
||||||
|
|
||||||
{user.bio && (
|
{user.bio && (
|
||||||
<div className="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