From 85bc6a1ad90dddbf6eaa969cdb965b8376cb6bfd Mon Sep 17 00:00:00 2001
From: glpshchn <464976@niuitmo.ru>
Date: Thu, 1 Jan 2026 22:39:12 +0300
Subject: [PATCH] Update files
---
CROSS_DOMAIN_SETUP.md | 101 ++++++++
EMAIL_SETUP.md | 102 ++++++++
REBUILD_FRONTEND.md | 64 +++++
backend/middleware/auth.js | 123 +++++++++
backend/middleware/security.js | 2 +-
backend/models/User.js | 5 +
backend/routes/auth.js | 29 ++-
backend/routes/posts.js | 16 +-
backend/routes/users.js | 22 +-
backend/utils/email.js | 37 ++-
frontend/src/App.jsx | 4 +-
frontend/src/components/FullPlayer.jsx | 4 +-
frontend/src/components/MiniPlayer.jsx | 4 +-
frontend/src/components/MusicAttachment.jsx | 4 +-
frontend/src/components/MusicPickerModal.jsx | 4 +-
frontend/src/contexts/MusicPlayerContext.jsx | 6 +-
frontend/src/pages/Feed.css | 32 +++
frontend/src/pages/Feed.jsx | 87 +++++--
frontend/src/pages/MediaMusic.jsx | 4 +-
frontend/src/pages/Profile.css | 252 +++++++++++++++++++
frontend/src/pages/Profile.jsx | 152 ++++++++++-
frontend/src/pages/VerifyEmail.css | 135 ++++++++++
frontend/src/pages/VerifyEmail.jsx | 219 ++++++++++++++++
frontend/src/utils/api.js | 31 ++-
24 files changed, 1370 insertions(+), 69 deletions(-)
create mode 100644 CROSS_DOMAIN_SETUP.md
create mode 100644 EMAIL_SETUP.md
create mode 100644 REBUILD_FRONTEND.md
create mode 100644 frontend/src/pages/VerifyEmail.css
create mode 100644 frontend/src/pages/VerifyEmail.jsx
diff --git a/CROSS_DOMAIN_SETUP.md b/CROSS_DOMAIN_SETUP.md
new file mode 100644
index 0000000..d5d6e9d
--- /dev/null
+++ b/CROSS_DOMAIN_SETUP.md
@@ -0,0 +1,101 @@
+# 🌐 Настройка для разных доменов (Frontend и API)
+
+## Ситуация
+- **Frontend**: `nakama.glpshchn.ru` (старый домен)
+- **API**: `nkm.guru` (новый домен)
+
+## ✅ Что нужно сделать
+
+### 1. Обновить CORS на backend
+
+В файле `.env` на сервере добавьте оба домена:
+
+```bash
+# Разрешить запросы с обоих доменов
+CORS_ORIGIN=https://nakama.glpshchn.ru,https://nkm.guru
+```
+
+Или если хотите разрешить все домены (менее безопасно, но проще):
+
+```bash
+CORS_ORIGIN=*
+```
+
+### 2. Обновить Content Security Policy
+
+В `backend/middleware/security.js` нужно разрешить подключения к новому API домену:
+
+```javascript
+connectSrc: ["'self'", "https://api.telegram.org", "https://e621.net", "https://gelbooru.com", "https://nkm.guru"],
+```
+
+### 3. Пересобрать frontend с новым API URL
+
+```bash
+cd /var/www/nakama/frontend
+
+# Установить переменную окружения для сборки
+export VITE_API_URL=https://nkm.guru/api
+
+# Пересобрать frontend
+npm run build
+
+# Перезапустить nginx
+sudo systemctl reload nginx
+```
+
+### 4. Перезапустить backend
+
+```bash
+# Если используете PM2:
+pm2 restart nakama-backend
+
+# Или если используете Docker:
+docker-compose restart backend
+```
+
+## 🔍 Проверка
+
+После настройки проверьте:
+
+1. **CORS работает**:
+ ```bash
+ curl -H "Origin: https://nakama.glpshchn.ru" \
+ -H "Access-Control-Request-Method: GET" \
+ -H "Access-Control-Request-Headers: Content-Type" \
+ -X OPTIONS \
+ https://nkm.guru/api/health
+ ```
+
+2. **В консоли браузера** (F12 → Network):
+ - Запросы должны идти на: `https://nkm.guru/api/...`
+ - Headers должны содержать: `Access-Control-Allow-Origin: https://nakama.glpshchn.ru`
+
+## ⚠️ Важно
+
+1. **Cookies**: Если используете cookies для авторизации, убедитесь, что:
+ - `withCredentials: true` в axios (уже настроено)
+ - `credentials: true` в CORS (уже настроено)
+ - Cookies должны быть с правильным `SameSite` и `Secure` флагами
+
+2. **SSL сертификаты**: Оба домена должны иметь валидные SSL сертификаты
+
+3. **Telegram Mini App**: Если используете Telegram Mini App, убедитесь, что домен frontend добавлен в настройки бота
+
+## 🔄 Альтернативное решение: Проксирование через nginx
+
+Если не хотите настраивать CORS, можно настроить проксирование на старом домене:
+
+```nginx
+# В nginx конфигурации для nakama.glpshchn.ru
+location /api {
+ proxy_pass https://nkm.guru/api;
+ proxy_set_header Host nkm.guru;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+}
+```
+
+Тогда frontend будет использовать относительный путь `/api`, а nginx будет проксировать запросы на новый домен.
+
diff --git a/EMAIL_SETUP.md b/EMAIL_SETUP.md
new file mode 100644
index 0000000..5c9525a
--- /dev/null
+++ b/EMAIL_SETUP.md
@@ -0,0 +1,102 @@
+# 📧 Настройка отправки email
+
+## Проблема
+Ошибка: `Missing credentials in config` при отправке magic-link email.
+
+## ✅ Решение
+
+### Вариант 1: Настроить AWS SES (если используете AWS)
+
+В файле `.env` на сервере добавьте:
+
+```bash
+EMAIL_PROVIDER=aws
+AWS_SES_ACCESS_KEY_ID=your_access_key_id
+AWS_SES_SECRET_ACCESS_KEY=your_secret_access_key
+AWS_SES_REGION=us-east-1
+EMAIL_FROM=noreply@nakama.guru
+```
+
+**Важно:**
+- `EMAIL_FROM` должен быть верифицированным email в AWS SES
+- Для production нужно выйти из sandbox режима AWS SES
+
+### Вариант 2: Использовать Yandex SMTP (рекомендуется для начала)
+
+В файле `.env` на сервере:
+
+```bash
+EMAIL_PROVIDER=yandex
+YANDEX_SMTP_HOST=smtp.yandex.ru
+YANDEX_SMTP_PORT=465
+YANDEX_SMTP_SECURE=true
+YANDEX_SMTP_USER=your-email@yandex.ru
+YANDEX_SMTP_PASSWORD=your_app_password
+EMAIL_FROM=your-email@yandex.ru
+```
+
+**Важно для Yandex:**
+- Используйте **пароль приложения**, а не основной пароль аккаунта
+- Пароль приложения создается в настройках безопасности Yandex
+- Порт 465 для SSL, порт 587 для STARTTLS
+
+### Вариант 3: Использовать любой SMTP сервер
+
+```bash
+EMAIL_PROVIDER=smtp
+SMTP_HOST=smtp.example.com
+SMTP_PORT=587
+SMTP_SECURE=false
+SMTP_USER=your-email@example.com
+SMTP_PASSWORD=your_password
+EMAIL_FROM=your-email@example.com
+```
+
+## 🔧 После настройки
+
+1. **Перезапустите backend:**
+ ```bash
+ pm2 restart nakama-backend
+ # или
+ docker-compose restart backend
+ ```
+
+2. **Проверьте логи:**
+ ```bash
+ pm2 logs nakama-backend --lines 50
+ ```
+
+ Должно появиться:
+ - `[Email] ✅ AWS SES клиент инициализирован` (для AWS)
+ - или `[Email] ✅ SMTP transporter инициализирован` (для SMTP)
+
+3. **Проверьте отправку:**
+ - Попробуйте отправить magic-link через форму авторизации
+ - Проверьте логи на наличие ошибок
+
+## ⚠️ Частые проблемы
+
+### 1. "Missing credentials"
+- **Причина:** Не установлены AWS credentials
+- **Решение:** Установите `AWS_SES_ACCESS_KEY_ID` и `AWS_SES_SECRET_ACCESS_KEY` или используйте `EMAIL_PROVIDER=yandex`
+
+### 2. "EAUTH" ошибка для Yandex
+- **Причина:** Используется основной пароль вместо пароля приложения
+- **Решение:** Создайте пароль приложения в настройках безопасности Yandex
+
+### 3. "ECONNECTION" ошибка
+- **Причина:** Неверный хост или порт SMTP
+- **Решение:** Проверьте `YANDEX_SMTP_HOST` и `YANDEX_SMTP_PORT`
+
+## 📝 Пример .env для Yandex
+
+```bash
+EMAIL_PROVIDER=yandex
+YANDEX_SMTP_HOST=smtp.yandex.ru
+YANDEX_SMTP_PORT=465
+YANDEX_SMTP_SECURE=true
+YANDEX_SMTP_USER=aaem9848@yandex.ru
+YANDEX_SMTP_PASSWORD=your_app_password_here
+EMAIL_FROM=aaem9848@yandex.ru
+```
+
diff --git a/REBUILD_FRONTEND.md b/REBUILD_FRONTEND.md
new file mode 100644
index 0000000..e560594
--- /dev/null
+++ b/REBUILD_FRONTEND.md
@@ -0,0 +1,64 @@
+# 🔄 Инструкция по пересборке frontend после смены домена
+
+## Проблема
+Vite встраивает переменные окружения (`VITE_API_URL`) в код во время сборки. Если frontend был собран со старым доменом, он будет продолжать использовать старый домен даже после изменения `.env`.
+
+## ✅ Решение
+
+### Вариант 1: Пересборка frontend (рекомендуется)
+
+```bash
+# Перейти в директорию frontend
+cd /var/www/nakama/frontend
+
+# Установить переменную окружения для сборки
+export VITE_API_URL=https://nkm.guru/api
+
+# Пересобрать frontend
+npm run build
+
+# Если используете PM2 или другой процесс-менеджер, перезапустите nginx
+sudo systemctl reload nginx
+```
+
+### Вариант 2: Использование относительного пути (уже исправлено в коде)
+
+Код уже обновлен так, чтобы в production всегда использовался относительный путь `/api`, который работает с любым доменом. Но если frontend был собран со старым `VITE_API_URL`, нужно пересобрать.
+
+### Вариант 3: Docker
+
+Если используете Docker:
+
+```bash
+cd /var/www/nakama
+
+# Пересобрать frontend с новым доменом
+docker-compose build frontend --build-arg VITE_API_URL=https://nkm.guru/api
+
+# Или установить в .env и пересобрать:
+# VITE_API_URL=https://nkm.guru/api
+docker-compose build frontend
+docker-compose up -d frontend
+```
+
+## 🔍 Проверка после пересборки
+
+1. **Очистите кэш браузера** (Ctrl+Shift+Delete или Cmd+Shift+Delete)
+2. **Или используйте Hard Refresh** (Ctrl+F5 или Cmd+Shift+R)
+3. **Проверьте в консоли браузера** (F12 → Network):
+ - Запросы должны идти на: `https://nkm.guru/api/...`
+ - НЕ должно быть: `https://nakama.glpshchn.ru/api/...`
+
+## 📝 Важно
+
+После пересборки frontend будет использовать относительный путь `/api` в production, что означает:
+- ✅ Работает с любым доменом автоматически
+- ✅ Не нужно пересобирать при смене домена
+- ✅ Использует текущий домен браузера
+
+## 🚀 Быстрая команда для пересборки
+
+```bash
+cd /var/www/nakama/frontend && npm run build && sudo systemctl reload nginx
+```
+
diff --git a/backend/middleware/auth.js b/backend/middleware/auth.js
index 3b0facc..51ffefe 100644
--- a/backend/middleware/auth.js
+++ b/backend/middleware/auth.js
@@ -386,6 +386,128 @@ const authenticateJWT = async (req, res, next) => {
}
};
+// Optional authenticate - позволяет гостям, но устанавливает req.user если есть авторизация
+const authenticateOptional = async (req, res, next) => {
+ try {
+ const authHeader = req.headers.authorization || '';
+ const guestId = req.headers['x-guest-id'];
+
+ // Если есть guest_id, создаем гостевого пользователя
+ if (guestId && !authHeader) {
+ req.user = {
+ isGuest: true,
+ id: guestId,
+ settings: {
+ whitelist: {
+ noNSFW: true,
+ noHomo: true
+ },
+ searchPreference: 'furry'
+ }
+ };
+ return next();
+ }
+
+ // Пытаемся авторизовать через Telegram или JWT
+ let initDataRaw = null;
+ let token = null;
+
+ if (authHeader.startsWith('tma ')) {
+ initDataRaw = authHeader.slice(4).trim();
+ } else if (authHeader.startsWith('Bearer ')) {
+ token = authHeader.slice(7);
+ }
+
+ if (!initDataRaw && !token) {
+ const headerInitData = req.headers['x-telegram-init-data'];
+ if (headerInitData && typeof headerInitData === 'string') {
+ initDataRaw = headerInitData.trim();
+ }
+ }
+
+ // Если нет данных для авторизации, создаем гостя
+ if (!initDataRaw && !token) {
+ req.user = {
+ isGuest: true,
+ id: guestId || `guest_${Date.now()}`,
+ settings: {
+ whitelist: {
+ noNSFW: true,
+ noHomo: true
+ },
+ searchPreference: 'furry'
+ }
+ };
+ return next();
+ }
+
+ // Пытаемся авторизовать через Telegram
+ if (initDataRaw) {
+ try {
+ const payload = validateAndParseInitData(initDataRaw);
+ const telegramUser = payload.user;
+ const normalizedUser = normalizeTelegramUser(telegramUser);
+
+ let user = await User.findOne({ telegramId: normalizedUser.id.toString() });
+ if (user && !user.banned) {
+ await ensureUserSettings(user);
+ await touchUserActivity(user);
+ req.user = user;
+ req.telegramUser = normalizedUser;
+ return next();
+ }
+ } catch (error) {
+ // Игнорируем ошибки Telegram авторизации, продолжаем как гость
+ }
+ }
+
+ // Пытаемся авторизовать через JWT
+ if (token) {
+ try {
+ const { verifyAccessToken } = require('../utils/tokens');
+ const payload = verifyAccessToken(token);
+ const user = await User.findById(payload.userId);
+
+ if (user && !user.banned) {
+ req.user = user;
+ return next();
+ }
+ } catch (error) {
+ // Игнорируем ошибки JWT, продолжаем как гость
+ }
+ }
+
+ // Если авторизация не удалась, создаем гостя
+ req.user = {
+ isGuest: true,
+ id: guestId || `guest_${Date.now()}`,
+ settings: {
+ whitelist: {
+ noNSFW: true,
+ noHomo: true
+ },
+ searchPreference: 'furry'
+ }
+ };
+ next();
+ } catch (error) {
+ console.error('Ошибка optional auth:', error);
+ // В случае ошибки создаем гостя
+ req.user = {
+ isGuest: true,
+ id: req.headers['x-guest-id'] || `guest_${Date.now()}`,
+ settings: {
+ whitelist: {
+ noNSFW: true,
+ noHomo: true
+ },
+ searchPreference: 'furry'
+ }
+ };
+ next();
+ }
+};
+
// Комбинированный middleware: Telegram или JWT
const authenticateModerationFlexible = async (req, res, next) => {
// Попробовать Telegram авторизацию
@@ -400,6 +522,7 @@ const authenticateModerationFlexible = async (req, res, next) => {
};
module.exports = {
+ authenticateOptional,
authenticate,
authenticateModeration,
authenticateJWT,
diff --git a/backend/middleware/security.js b/backend/middleware/security.js
index 67feb7c..92d18d0 100644
--- a/backend/middleware/security.js
+++ b/backend/middleware/security.js
@@ -13,7 +13,7 @@ const helmetConfig = helmet({
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'", "https://telegram.org", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:", "blob:"],
- connectSrc: ["'self'", "https://api.telegram.org", "https://e621.net", "https://gelbooru.com"],
+ connectSrc: ["'self'", "https://api.telegram.org", "https://e621.net", "https://gelbooru.com", "https://nkm.guru"],
fontSrc: ["'self'", "data:"],
objectSrc: ["'none'"],
// Запретить использование консоли и eval
diff --git a/backend/models/User.js b/backend/models/User.js
index bb91af2..539b714 100644
--- a/backend/models/User.js
+++ b/backend/models/User.js
@@ -108,6 +108,11 @@ const UserSchema = new mongoose.Schema({
// Magic-link токены для авторизации
magicLinkToken: String,
magicLinkExpires: Date,
+ // Onboarding - отслеживание показа приветствия
+ onboardingCompleted: {
+ type: Boolean,
+ default: false
+ },
createdAt: {
type: Date,
default: Date.now
diff --git a/backend/routes/auth.js b/backend/routes/auth.js
index a0154ba..e980649 100644
--- a/backend/routes/auth.js
+++ b/backend/routes/auth.js
@@ -59,7 +59,8 @@ const respondWithUser = async (user, res) => {
following: populatedUser.following,
tickets: populatedUser.tickets || 0,
settings,
- banned: populatedUser.banned
+ banned: populatedUser.banned,
+ onboardingCompleted: populatedUser.onboardingCompleted || false
}
});
};
@@ -454,7 +455,7 @@ router.get('/magic-link/verify', async (req, res) => {
// Установка пароля при регистрации через magic-link
router.post('/magic-link/set-password', async (req, res) => {
try {
- const { token, password, username } = req.body;
+ const { token, password, username, firstName } = req.body;
if (!token || !password) {
return res.status(400).json({ error: 'Токен и пароль обязательны' });
@@ -465,6 +466,20 @@ router.post('/magic-link/set-password', async (req, res) => {
return res.status(400).json({ error: 'Пароль должен быть от 8 до 24 символов' });
}
+ // Валидация username (обязателен, нельзя менять после регистрации)
+ if (!username || username.trim().length < 3 || username.trim().length > 20) {
+ return res.status(400).json({ error: 'Юзернейм обязателен и должен быть от 3 до 20 символов' });
+ }
+
+ // Проверить уникальность username (исключая текущего пользователя)
+ const existingUser = await User.findOne({
+ username: username.trim().toLowerCase(),
+ _id: { $ne: user._id }
+ });
+ if (existingUser) {
+ return res.status(400).json({ error: 'Этот юзернейм уже занят' });
+ }
+
// Найти пользователя с этим токеном
const user = await User.findOne({
magicLinkToken: token,
@@ -486,8 +501,14 @@ router.post('/magic-link/set-password', async (req, res) => {
user.magicLinkExpires = undefined;
user.lastActiveAt = new Date();
- if (username && !user.username) {
- user.username = username;
+ // Устанавливаем username (нельзя менять после регистрации)
+ if (username) {
+ user.username = username.trim().toLowerCase();
+ }
+
+ // Устанавливаем firstName (никнейм, можно менять)
+ if (firstName) {
+ user.firstName = firstName.trim();
}
await user.save();
diff --git a/backend/routes/posts.js b/backend/routes/posts.js
index 5fa4671..f38a81f 100644
--- a/backend/routes/posts.js
+++ b/backend/routes/posts.js
@@ -1,6 +1,6 @@
const express = require('express');
const router = express.Router();
-const { authenticate } = require('../middleware/auth');
+const { authenticate, authenticateOptional } = require('../middleware/auth');
const { postCreationLimiter, interactionLimiter } = require('../middleware/rateLimiter');
const { searchLimiter } = require('../middleware/rateLimiter');
const { validatePostContent, validateTags, validateImageUrl } = require('../middleware/validator');
@@ -46,11 +46,12 @@ router.get('/:id', authenticate, async (req, res) => {
}
});
-// Получить ленту постов
-router.get('/', authenticate, async (req, res) => {
+// Получить ленту постов (доступно для гостей)
+router.get('/', authenticateOptional, async (req, res) => {
try {
const { page = 1, limit = 20, tag, userId, filter = 'all' } = req.query;
const query = {};
+ const isGuest = req.user?.isGuest;
// Фильтр по тегу
if (tag) {
@@ -63,7 +64,7 @@ router.get('/', authenticate, async (req, res) => {
}
// Фильтры: 'all', 'interests', 'following'
- if (filter === 'interests') {
+ if (filter === 'interests' && !isGuest) {
// Лента по интересам - посты с тегами из preferredTags пользователя
const user = await User.findById(req.user._id).select('preferredTags');
if (user.preferredTags && user.preferredTags.length > 0) {
@@ -76,7 +77,7 @@ router.get('/', authenticate, async (req, res) => {
currentPage: page
});
}
- } else if (filter === 'following') {
+ } else if (filter === 'following' && !isGuest) {
// Лента подписок - посты от пользователей, на которых подписан
const user = await User.findById(req.user._id).select('following');
if (user.following && user.following.length > 0) {
@@ -90,13 +91,14 @@ router.get('/', authenticate, async (req, res) => {
});
}
}
+ // Для гостей или filter === 'all' - показываем все посты без дополнительных фильтров
// 'all' - все посты, без дополнительных фильтров
// Применить whitelist настройки пользователя (только NSFW и Homo)
- if (req.user.settings.whitelist.noNSFW) {
+ if (req.user?.settings?.whitelist?.noNSFW) {
query.isNSFW = false;
}
- if (req.user.settings.whitelist.noHomo) {
+ if (req.user?.settings?.whitelist?.noHomo) {
// Скрывать только посты, помеченные как гомосексуальные.
// Посты без флага (старые) остаются видимыми.
query.isHomo = { $ne: true };
diff --git a/backend/routes/users.js b/backend/routes/users.js
index 6e19187..19676d3 100644
--- a/backend/routes/users.js
+++ b/backend/routes/users.js
@@ -1,6 +1,7 @@
const express = require('express');
const router = express.Router();
const { authenticate } = require('../middleware/auth');
+const { uploadAvatar } = require('../middleware/upload');
const User = require('../models/User');
const Post = require('../models/Post');
const Notification = require('../models/Notification');
@@ -125,14 +126,31 @@ router.post('/:id/follow', authenticate, async (req, res) => {
});
// Обновить профиль
-router.put('/profile', authenticate, async (req, res) => {
+router.put('/profile', authenticate, uploadAvatar, async (req, res) => {
try {
- const { bio, settings } = req.body;
+ const { bio, settings, firstName, lastName, onboardingCompleted } = req.body;
+
+ // Обработка загруженной аватарки
+ if (req.uploadedFiles && req.uploadedFiles.length > 0) {
+ req.user.photoUrl = req.uploadedFiles[0];
+ }
if (bio !== undefined) {
req.user.bio = bio;
}
+ if (firstName !== undefined) {
+ req.user.firstName = firstName;
+ }
+
+ if (lastName !== undefined) {
+ req.user.lastName = lastName;
+ }
+
+ if (onboardingCompleted !== undefined) {
+ req.user.onboardingCompleted = onboardingCompleted;
+ }
+
if (settings) {
req.user.settings = req.user.settings || {};
diff --git a/backend/utils/email.js b/backend/utils/email.js
index 601a2e1..9a9ec20 100644
--- a/backend/utils/email.js
+++ b/backend/utils/email.js
@@ -12,6 +12,18 @@ const initializeEmailService = () => {
const emailProvider = process.env.EMAIL_PROVIDER || 'aws'; // aws, yandex, smtp
if (emailProvider === 'aws' && config.email?.aws) {
+ const accessKeyId = config.email.aws.accessKeyId;
+ const secretAccessKey = config.email.aws.secretAccessKey;
+
+ // Проверка наличия credentials
+ if (!accessKeyId || !secretAccessKey) {
+ console.error('[Email] ❌ AWS SES credentials не установлены!');
+ console.error('[Email] Установите AWS_SES_ACCESS_KEY_ID и AWS_SES_SECRET_ACCESS_KEY в .env');
+ console.error('[Email] Или используйте EMAIL_PROVIDER=yandex или EMAIL_PROVIDER=smtp');
+ sesClient = null;
+ return;
+ }
+
const awsRegion = config.email.aws.region || 'us-east-1';
const validAWSRegions = [
'us-east-1', 'us-east-2', 'us-west-1', 'us-west-2',
@@ -26,8 +38,8 @@ const initializeEmailService = () => {
(isYandexCloud ? 'https://postbox.cloud.yandex.net' : null);
const sesConfig = {
- accessKeyId: config.email.aws.accessKeyId,
- secretAccessKey: config.email.aws.secretAccessKey,
+ accessKeyId: accessKeyId,
+ secretAccessKey: secretAccessKey,
region: awsRegion
};
@@ -38,17 +50,15 @@ const initializeEmailService = () => {
sesConfig.isYandexCloud = true;
console.log(`[Email] Используется Yandex Cloud Postbox с endpoint: ${endpointUrl}`);
// Не создаем SES клиент для Yandex Cloud, будем использовать прямые HTTP запросы
+ sesClient = { config: sesConfig, isYandexCloud: true };
} else if (!validAWSRegions.includes(awsRegion)) {
console.warn(`[Email] Невалидный регион AWS SES: ${awsRegion}. Используется us-east-1`);
sesConfig.region = 'us-east-1';
sesClient = new AWS.SES(sesConfig);
+ console.log('[Email] ✅ AWS SES клиент инициализирован');
} else {
sesClient = new AWS.SES(sesConfig);
- }
-
- // Сохраняем конфигурацию для Yandex Cloud
- if (endpointUrl) {
- sesClient = { config: sesConfig, isYandexCloud: true };
+ console.log('[Email] ✅ AWS SES клиент инициализирован');
}
} else if (emailProvider === 'yandex' || emailProvider === 'smtp') {
const emailConfig = config.email?.[emailProvider] || config.email?.smtp || {};
@@ -141,7 +151,12 @@ const sendEmail = async (to, subject, html, text) => {
const emailProvider = process.env.EMAIL_PROVIDER || 'aws';
const fromEmail = process.env.EMAIL_FROM || config.email?.from || 'noreply@nakama.guru';
- if (emailProvider === 'aws' && sesClient) {
+ if (emailProvider === 'aws') {
+ if (!sesClient) {
+ const errorMsg = 'AWS SES не инициализирован. Проверьте AWS_SES_ACCESS_KEY_ID и AWS_SES_SECRET_ACCESS_KEY в .env файле. Или используйте EMAIL_PROVIDER=yandex или EMAIL_PROVIDER=smtp';
+ console.error('[Email] ❌', errorMsg);
+ throw new Error(errorMsg);
+ }
// Проверка на Yandex Cloud Postbox
if (sesClient.isYandexCloud) {
// Yandex Cloud Postbox использует SESv2 API - используем прямой HTTP запрос
@@ -246,7 +261,11 @@ const sendEmail = async (to, subject, html, text) => {
console.error('Ошибка отправки email:', error);
// Более информативные сообщения об ошибках
- if (error.code === 'EAUTH') {
+ if (error.code === 'CredentialsError' || (error.message && error.message.includes('Missing credentials'))) {
+ const errorMsg = 'AWS SES credentials не настроены. Установите AWS_SES_ACCESS_KEY_ID и AWS_SES_SECRET_ACCESS_KEY в .env файле. Или используйте EMAIL_PROVIDER=yandex или EMAIL_PROVIDER=smtp';
+ console.error('[Email] ❌', errorMsg);
+ throw new Error(errorMsg);
+ } else if (error.code === 'EAUTH') {
throw new Error('Неверные учетные данные SMTP. Проверьте YANDEX_SMTP_USER и YANDEX_SMTP_PASSWORD в .env файле. Для Yandex используйте пароль приложения, а не основной пароль.');
} else if (error.code === 'ECONNECTION') {
throw new Error('Не удалось подключиться к SMTP серверу. Проверьте YANDEX_SMTP_HOST и YANDEX_SMTP_PORT.');
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 58ad35c..e29a825 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -16,6 +16,7 @@ import Profile from './pages/Profile'
import UserProfile from './pages/UserProfile'
import CommentsPage from './pages/CommentsPage'
import PostMenuPage from './pages/PostMenuPage'
+import VerifyEmail from './pages/VerifyEmail'
import MiniPlayer from './components/MiniPlayer'
import FullPlayer from './components/FullPlayer'
import './styles/index.css'
@@ -222,7 +223,7 @@ function AppContent() {
{
diff --git a/frontend/src/components/MiniPlayer.jsx b/frontend/src/components/MiniPlayer.jsx
index 148d617..f3dc950 100644
--- a/frontend/src/components/MiniPlayer.jsx
+++ b/frontend/src/components/MiniPlayer.jsx
@@ -48,7 +48,9 @@ export default function MiniPlayer() {
{
diff --git a/frontend/src/components/MusicAttachment.jsx b/frontend/src/components/MusicAttachment.jsx
index d8fd90a..e0675be 100644
--- a/frontend/src/components/MusicAttachment.jsx
+++ b/frontend/src/components/MusicAttachment.jsx
@@ -27,7 +27,9 @@ export default function MusicAttachment({ track, onRemove, showRemove = false })
{
diff --git a/frontend/src/components/MusicPickerModal.jsx b/frontend/src/components/MusicPickerModal.jsx
index 3078b63..fdd6587 100644
--- a/frontend/src/components/MusicPickerModal.jsx
+++ b/frontend/src/components/MusicPickerModal.jsx
@@ -64,7 +64,9 @@ export default function MusicPickerModal({ onClose, onSelect }) {
{
diff --git a/frontend/src/contexts/MusicPlayerContext.jsx b/frontend/src/contexts/MusicPlayerContext.jsx
index ebf5438..2ae4edd 100644
--- a/frontend/src/contexts/MusicPlayerContext.jsx
+++ b/frontend/src/contexts/MusicPlayerContext.jsx
@@ -113,8 +113,10 @@ export const MusicPlayerProvider = ({ children }) => {
// Если относительный URL (локальное хранилище), добавить базовый URL
if (audioUrl.startsWith('/uploads/')) {
- const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:3000/api'
- audioUrl = apiUrl.replace('/api', '') + audioUrl
+ const baseUrl = import.meta.env.DEV
+ ? (import.meta.env.VITE_API_URL || 'http://localhost:3000/api').replace('/api', '')
+ : (import.meta.env.VITE_API_URL || 'https://nkm.guru/api').replace('/api', '')
+ audioUrl = baseUrl + audioUrl
}
// Убедиться что URL валидный
diff --git a/frontend/src/pages/Feed.css b/frontend/src/pages/Feed.css
index 23f78ab..d4e650d 100644
--- a/frontend/src/pages/Feed.css
+++ b/frontend/src/pages/Feed.css
@@ -30,12 +30,39 @@
align-items: center;
justify-content: center;
box-shadow: 0 2px 8px var(--shadow-md);
+ border: none;
+ cursor: pointer;
+ transition: transform 0.2s ease;
+}
+
+.create-btn:hover {
+ transform: scale(1.05);
+}
+
+.create-btn:active {
+ transform: scale(0.95);
}
.create-btn svg {
stroke: white;
}
+.create-btn.auth-btn {
+ width: auto;
+ height: 36px;
+ padding: 0 16px;
+ border-radius: 18px;
+ font-size: 15px;
+ font-weight: 600;
+ background: var(--button-accent);
+ color: white;
+}
+
+.create-btn.auth-btn:hover {
+ opacity: 0.9;
+ transform: none;
+}
+
[data-theme="dark"] .create-btn {
background: #FFFFFF;
color: #000000;
@@ -45,6 +72,11 @@
stroke: #000000;
}
+[data-theme="dark"] .create-btn.auth-btn {
+ background: var(--button-accent);
+ color: white;
+}
+
.feed-filters {
display: flex;
gap: 8px;
diff --git a/frontend/src/pages/Feed.jsx b/frontend/src/pages/Feed.jsx
index d8c0bf3..9125478 100644
--- a/frontend/src/pages/Feed.jsx
+++ b/frontend/src/pages/Feed.jsx
@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react'
import { useSearchParams, useNavigate } from 'react-router-dom'
-import { getPosts, getPost } from '../utils/api'
+import { getPosts, getPost, updateProfile } from '../utils/api'
import PostCard from '../components/PostCard'
import CreatePostModal from '../components/CreatePostModal'
import OnboardingPost from '../components/OnboardingPost'
@@ -9,7 +9,7 @@ import { Plus, Settings } from 'lucide-react'
import { hapticFeedback } from '../utils/telegram'
import './Feed.css'
-export default function Feed({ user }) {
+export default function Feed({ user, setUser }) {
const [searchParams] = useSearchParams()
const navigate = useNavigate()
const [posts, setPosts] = useState([])
@@ -21,12 +21,22 @@ export default function Feed({ user }) {
const [page, setPage] = useState(1)
const [hasMore, setHasMore] = useState(true)
const [highlightPostId, setHighlightPostId] = useState(null)
+ // Проверяем, показывалось ли приветствие ранее - только 1 раз (из БД)
const [onboardingVisible, setOnboardingVisible] = useState({
- welcome: true,
- tags: true,
- media: true
+ welcome: !user?.onboardingCompleted && (user?.isGuest || !user?.onboardingCompleted),
+ tags: false, // Удаляем второй пост
+ media: false
})
+ useEffect(() => {
+ // Обновляем onboarding при изменении user (из БД)
+ setOnboardingVisible({
+ welcome: !user?.onboardingCompleted && (user?.isGuest || !user?.onboardingCompleted),
+ tags: false,
+ media: false
+ })
+ }, [user])
+
useEffect(() => {
// Проверить параметр post в URL
const postId = searchParams.get('post')
@@ -155,7 +165,7 @@ export default function Feed({ user }) {
setShowCreateModal(false)
}
- const handleOnboardingAction = (type) => {
+ const handleOnboardingAction = async (type) => {
hapticFeedback('light')
if (type === 'welcome' || type === 'tags') {
@@ -164,18 +174,39 @@ export default function Feed({ user }) {
navigate('/media')
}
+ // Сохраняем в БД, если пользователь авторизован
+ if (type === 'welcome' && !user?.isGuest && user?.id) {
+ try {
+ await updateProfile({ onboardingCompleted: true })
+ // Обновляем локальное состояние пользователя
+ if (setUser) {
+ setUser({ ...user, onboardingCompleted: true })
+ }
+ } catch (error) {
+ console.error('Ошибка сохранения onboarding:', error)
+ }
+ }
+
setOnboardingVisible(prev => ({ ...prev, [type]: false }))
- const dismissed = JSON.parse(localStorage.getItem('onboarding_dismissed') || '{}')
- dismissed[type] = true
- localStorage.setItem('onboarding_dismissed', JSON.stringify(dismissed))
}
- const handleOnboardingDismiss = (type) => {
+ const handleOnboardingDismiss = async (type) => {
hapticFeedback('light')
+
+ // Сохраняем в БД, если пользователь авторизован
+ if (type === 'welcome' && !user?.isGuest && user?.id) {
+ try {
+ await updateProfile({ onboardingCompleted: true })
+ // Обновляем локальное состояние пользователя
+ if (setUser) {
+ setUser({ ...user, onboardingCompleted: true })
+ }
+ } catch (error) {
+ console.error('Ошибка сохранения onboarding:', error)
+ }
+ }
+
setOnboardingVisible(prev => ({ ...prev, [type]: false }))
- const dismissed = JSON.parse(localStorage.getItem('onboarding_dismissed') || '{}')
- dismissed[type] = true
- localStorage.setItem('onboarding_dismissed', JSON.stringify(dismissed))
}
const handleLoadMore = () => {
@@ -189,9 +220,23 @@ export default function Feed({ user }) {
{/* Хедер */}
Пароль должен быть от 8 до 24 символов
+Проверка ссылки...
+{error}
+ +Установите пароль и заполните данные профиля
+ + +