Update files

This commit is contained in:
glpshchn 2025-12-01 03:51:23 +03:00
parent fb12c0626b
commit ed85d8f6db
15 changed files with 853 additions and 49 deletions

332
FIX_MONGODB.md Normal file
View File

@ -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 - это безопасно, надежно и бесплатно для малых проектов!

113
MINIO_403_FIX.md Normal file
View File

@ -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!

View File

@ -119,7 +119,8 @@ function scheduleAvatarUpdates() {
module.exports = {
scheduleAvatarUpdates,
updateAllUserAvatars
updateAllUserAvatars,
fetchLatestAvatar
};

View File

@ -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;
// Обновлять только если есть новые данные, не перезаписывать существующие пустыми значениями
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.lastName = telegramUser.last_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;
// Обновлять только если есть новые данные, не перезаписывать существующие пустыми значениями
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.lastName = telegramUser.last_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
};

View File

@ -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;
// Обновлять только если есть новые данные, не перезаписывать существующие пустыми значениями
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.lastName = telegramUser.last_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' },

View File

@ -259,7 +259,8 @@ app.use(errorHandler);
// Инициализировать WebSocket
initWebSocket(server);
scheduleAvatarUpdates();
// Автообновление аватарок отключено - обновление происходит только при перезаходе
// scheduleAvatarUpdates();
startServerMonitorBot();
// Обработка необработанных ошибок

167
check-post-creation.md Normal file
View File

@ -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
```
Теперь попробуйте создать пост!

54
fix-minio-public.sh Normal file
View File

@ -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"

View File

@ -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>

View File

@ -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>

View File

@ -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)}

View File

@ -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>

View File

@ -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">

View File

@ -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">

19
minio-public-policy.json Normal file
View File

@ -0,0 +1,19 @@
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": ["*"]
},
"Action": [
"s3:GetObject"
],
"Resource": [
"arn:aws:s3:::nakama-media/*"
]
}
]
}