Skip to content

mkh1n/play-pulse

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

67 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

PlayPulse — платформа для открытия игр

PlayPulse — full-stack веб-приложение для поиска, каталогизации и открытия игр. Главная фишка — механика свайпов в стиле Tinder: пользователь листает карточки игр, ставит лайки/дизлайки, оценивает их, ведёт списки прохождения и покупок, а также читает агрегированную ленту игровых новостей (RSS).

Проект — это монорепозиторий из двух приложений:

  • apps/backend — REST API на NestJS 11 (порт 3001).
  • apps/frontend — веб-клиент на Next.js 16 / React 18 (порт 3000).

Данные хранятся в Supabase (PostgreSQL), ответы внешнего игрового API кэшируются в Redis, а данные об играх берутся из RAWG API через отдельный proxy на Vercel.

Документация написана по фактическому коду репозитория. Места, где поведение неочевидно или где в коде есть расхождения, помечены значком ⚠️.


📋 Оглавление

  1. Архитектура
  2. Технологический стек
  3. Структура репозитория
  4. Backend (NestJS)
  5. Frontend (Next.js)
  6. Аутентификация и JWT
  7. Система свайпов
  8. Лента новостей (RSS)
  9. База данных (Supabase)
  10. Кэширование (Redis)
  11. Переменные окружения
  12. Запуск проекта
  13. Make-команды
  14. Известные нюансы и техдолг

🏗 Архитектура

Поток данных

┌──────────┐   /api/*    ┌────────────────────────┐   /games,/auth,…   ┌──────────────┐   /api/rawg   ┌───────────┐
│ Browser  │ ──────────▶ │  Next.js (порт 3000)   │ ─────────────────▶ │ NestJS API   │ ───────────▶  │ RAWG Proxy│
│ (React)  │             │  • Route Handlers      │                    │ (порт 3001)  │               │ (Vercel)  │
│          │ ◀────────── │  • next.config rewrite │ ◀───────────────── │              │ ◀───────────  └─────┬─────┘
└──────────┘             └────────────────────────┘                    └──────┬───────┘                     │
                                                                              │                       ┌─────▼─────┐
                                                                       ┌──────▼───────┐               │  RAWG API │
                                                                       │ Supabase  +  │               └───────────┘
                                                                       │   Redis      │
                                                                       └──────────────┘

Ключевая идея: фронтенд ничего не запрашивает напрямую

Клиент (браузер) никогда не обращается ни к NestJS-бэкенду, ни к RAWG напрямую. Всё идёт через /api/* на самом Next.js-сервере, который выступает proxy-слоем:

  1. Frontend proxy — Next.js перенаправляет /api/* на NestJS. Делается это двумя способами одновременно (см. Два механизма проксирования): достаёт httpOnly-cookie с JWT, добавляет заголовок Authorization и форвардит запрос.
  2. Backend proxy — NestJS обращается к RAWG не напрямую, а через внешний proxy https://playpulse-rawg-proxy.vercel.app/api/rawg, где спрятан ключ RAWG.

Зачем так:

  • API-ключ RAWG не попадает в браузер.
  • JWT хранится в httpOnly-cookie и не доступен JS на клиенте.
  • Нет проблем с CORS.
  • Появляется единая точка для кэширования и обработки ошибок.

🛠 Технологический стек

Backend (apps/backend)

Технология Версия (package.json) Назначение
NestJS ^11 (@nestjs/*) Основной фреймворк
TypeScript ^5.7 Язык
@supabase/supabase-js ^2.89 Клиент PostgreSQL/Supabase
@nestjs/passport + passport-jwt ^11 / ^4 JWT-аутентификация
@nestjs/jwt ^11 Подпись/проверка токенов
bcrypt ^6 Хеширование паролей
@nestjs/cache-manager + cache-manager-redis-yet ^3 / ^5 Кэширование в Redis
@nestjs/axios + rxjs ^4 / ^7 HTTP-запросы к RAWG proxy
class-validator + class-transformer ^0.14 / ^0.5 Валидация DTO
@nestjs/swagger ^11 Декораторы для документации ⚠️ см. нюансы
Jest ^30 Тесты

Frontend (apps/frontend)

Технология Версия (package.json) Назначение
Next.js ^16 (App Router) React-фреймворк, SSR, API routes
React / React DOM 18.3.1 UI
TypeScript ^5 Язык
Tailwind CSS ^4 (@tailwindcss/postcss) Утилитарные стили
CSS Modules Стили компонентов (*.module.css)
zustand ^5 Хранилище состояния (объявлено в зависимостях)
recharts ^3 Графики (в зависимостях; статистика рисуется и кастомным SVG)
lucide-react, react-icons Иконки
use-debounce ^10 Debounce для поиска
react-paginate ^8 Пагинация каталога
cheerio, fast-xml-parser, @xmldom/xmldom Парсинг RSS на сервере
sharp ^0.34 Обрезка изображений (/api/crop-image)
@supabase/supabase-js ^2 Клиент Supabase (для изображений профиля)

Инфраструктура

Сервис Назначение
Supabase PostgreSQL для пользователей и их действий
Redis Кэш ответов RAWG (через cache-manager)
Vercel Хостинг RAWG proxy и image proxy
Docker Compose Локальный запуск redis + backend + frontend

📁 Структура репозитория

play-pulse/
├── apps/
│   ├── backend/                        # NestJS API (порт 3001)
│   │   ├── src/
│   │   │   ├── main.ts                  # Bootstrap: CORS, ValidationPipe, фильтр ошибок
│   │   │   ├── app.module.ts            # Корневой модуль + CacheModule(Redis) + middleware
│   │   │   ├── app.controller.ts        # GET / → "Hello World"
│   │   │   ├── auth/                    # Регистрация, вход, JWT-стратегия, guards
│   │   │   ├── users/                   # Профиль пользователя, "мои игры"
│   │   │   ├── games/                   # Каталог, детали, действия, похожие/популярные
│   │   │   │   └── rawg-proxy.ts        # ⭐ функция запроса к RAWG proxy
│   │   │   ├── swipes/                  # Выдача случайных игр для свайпов + batch-действия
│   │   │   ├── preferences/             # Сервис записи действий в БД (used by games/swipes/users)
│   │   │   ├── supabase/                # Глобальный модуль клиента Supabase
│   │   │   └── common/filters/          # HttpExceptionFilter + RequestLoggerMiddleware
│   │   ├── test/                        # e2e-тест (app.e2e-spec.ts)
│   │   └── Dockerfile
│   │
│   └── frontend/                       # Next.js (порт 3000)
│       ├── next.config.ts              # rewrites /api/* → backend, настройки next/image
│       ├── middleware.ts               # Защита /profile (редирект на /auth без токена)
│       ├── src/
│       │   ├── app/
│       │   │   ├── api/                 # ⭐ PROXY LAYER (route handlers)
│       │   │   ├── page.tsx             # Главная
│       │   │   ├── games/               # Каталог + детали игры
│       │   │   ├── swipes/              # Свайпы
│       │   │   ├── profile/             # Профиль (защищён middleware)
│       │   │   ├── news/                # Лента новостей
│       │   │   ├── auth/                # Вход/регистрация
│       │   │   ├── layout.tsx           # Корневой layout
│       │   │   └── ClientLayout.tsx     # Провайдеры контекстов + навигация
│       │   ├── components/              # UI-компоненты
│       │   ├── contexts/               # AuthContext, GameActionsContexts
│       │   ├── hooks/useMediaActions.ts
│       │   ├── services/               # gameService, scrollService
│       │   └── lib/                     # supabase.ts, platforms.ts
│       └── Dockerfile
│
├── docker-compose.yml                  # redis + backend + frontend
├── Makefile                            # команды запуска/сборки
└── README.md

🔧 Backend (NestJS)

Точка входа и глобальные настройки

Файл src/main.ts:

  • CORS — разрешён origin из FRONTEND_URL (по умолчанию http://localhost:3000), с credentials: true и заголовками Content-Type, Authorization.
  • ValidationPipe глобально с whitelist: true, transform: true, forbidNonWhitelisted: true — лишние поля в теле запроса приводят к ошибке 400.
  • HttpExceptionFilter глобально — единый формат ответа об ошибке.
  • PORT валидируется (1–65535), иначе процесс падает с понятным сообщением.

В app.module.ts подключены:

  • ConfigModule.forRoot({ isGlobal: true }) — чтение .env.
  • CacheModule.registerAsync({ isGlobal: true }) с Redis-стором (cache-manager-redis-yet, URL из REDIS_URL).
  • Модули: Supabase, Auth, Users, Games, Swipes.
  • RequestLoggerMiddleware логирует все входящие запросы (forRoutes('*')).

Модули Backend

Модуль Каталог Ответственность
AuthModule auth/ Регистрация, вход, проверка токена, JWT-стратегия, guards
UsersModule users/ Профиль (/users/me), публичный профиль, список «мои игры»
GamesModule games/ Каталог, детали, лайк/дизлайк/wishlist/оценка/статусы, похожие, популярные
SwipesModule swipes/ Случайные игры для свайпов + batch-обработка действий
PreferencesModule preferences/ PreferencesService — запись действий в БД (переиспользуется другими модулями)
SupabaseModule supabase/ Глобальный провайдер SupabaseClient
CommonModule (файлы) common/filters/ Глобальный фильтр исключений + middleware-логгер

⚠️ PreferencesService и PreferencesController физически лежат в preferences/, но регистрируются внутри SwipesModule (см. swipes.module.ts), а отдельного PreferencesModule в app.module.ts нет. Контроллер /preferences/game-actions поднимается через SwipesModule.

📡 API Endpoints Backend

Базовый адрес: http://localhost:3001. Защищённые маршруты требуют заголовок Authorization: Bearer <jwt> (Passport-стратегия jwt).

Auth (/auth)

Метод Endpoint Auth Описание
POST /auth/register Регистрация. Тело: { login, password }. Возвращает { user, token }
POST /auth/login Вход. Тело: { login, password }. Возвращает { user, token }
GET /auth/validate Проверка валидности токена (guard jwt)
POST /auth/logout Возвращает { message: 'Logged out successfully' }; токенов не инвалидирует

POST /auth/logout на бэкенде ничего не делает (JWT stateless). Реальный выход происходит на стороне Next.js — удалением httpOnly-cookie token/user.

Ответ register/login:

{
  "user": { "id": 1, "login": "gamemaster", "created_at": "..." },
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

Валидация (DTO):

  • login — строка, 3–30 символов, только [a-zA-Z0-9_] (только при регистрации).
  • password — строка, 6–50 символов.

Games (/games)

Метод Endpoint Auth Кэш Описание
GET /games 300 c Список игр с фильтрами
GET /games/popular Популярные игры (по -added)
GET /games/similar/:gameId Похожие игры (по жанрам)
GET /games/:id 3600 c Детали игры + скриншоты
POST/DELETE /games/:id/like Поставить / убрать лайк
POST/DELETE /games/:id/dislike Поставить / убрать дизлайк
POST/DELETE /games/:id/wishlist Добавить / убрать из wishlist
POST/DELETE /games/:id/rate Оценить (1–10) / удалить оценку
GET /games/:id/my-rating Моя оценка игры
POST /games/:id/status Статус прохождения
POST /games/:id/purchase Статус покупки

Кэширование GET /games и GET /games/:id реализовано через CacheInterceptor

  • @CacheTTL(...) (хранилище — Redis).

Query-параметры GET /games:

Параметр По умолчанию Описание
page 1 Номер страницы
pageSize 20 Размер страницы
search Поиск по названию
ordering -rating Сортировка (-rating, -added, -released, …)
genres ID/slug жанров через запятую
platforms ID платформ
tags ID тегов
dates Диапазон дат, напр. 2020-01-01,2020-12-31
developers ID разработчиков
publishers ID издателей

Тело для действий (like/dislike/wishlist) — GameMetaDto:

{
  "gameName": "Cyberpunk 2077",
  "gameImage": "https://media.rawg.io/.../game.jpg",
  "genres": [{ "id": 4, "name": "Action" }],
  "tags": [{ "id": 31, "name": "Singleplayer" }]
}

Для оценки — RateGameDto: те же поля + rating: number (1–10). Для статусов — UpdateGameStatusDto (status: not_played|playing|completed|dropped) и UpdatePurchaseDto (purchase: owned|not_owned|want_to_buy).

Swipes (/swipes)

Метод Endpoint Auth Описание
GET /swipes Случайные игры для свайпов. Query: limit (10), exclude (CSV id)
POST /swipes/swipe-action/batch Пачка действий { actions: [{ gameId, action, gameName, ... }] }

Ответ GET /swipes:

{ "success": true, "games": [ /* … */ ], "hasMore": true }

Users (/users)

Метод Endpoint Auth Описание
GET /users/me { user, profile } текущего пользователя
PUT /users/me Обновить логин/пароль
PUT /users/me/profile Обновить name, avatar_url, bio
GET /users/me/games Все действия пользователя (полные записи)
GET /users/:id Публичный профиль

Preferences (/preferences)

Метод Endpoint Auth Кэш Описание
GET /preferences/game-actions 60 c Компактный список действий пользователя

game-actions возвращает урезанный набор полей (game_id, action_type, rating, completion_status, purchase_status, лимит 500), который фронтенд раскладывает в карту состояний игр.

🔌 RAWG Proxy

Файл games/rawg-proxy.ts — единственная точка обращения к RAWG.

const RAWG_PROXY_URL = 'https://playpulse-rawg-proxy.vercel.app/api/rawg';

export async function fetchFromRawgProxy(httpService, endpoint, params?) {
  // чистит пустые параметры, собирает query, делает GET с таймаутом 15с,
  // логирует URL и подробности ошибки, пробрасывает исключение наверх
}

Особенности:

  • Пустые/null/'' параметры отбрасываются перед формированием query.
  • Таймаут запроса — 15 секунд.
  • Сервисы (GamesService, SwipesService) ловят ошибку и возвращают «мягкий» результат (пустой список / fallback), чтобы не ронять API.

Фильтрация качества и похожие игры

GamesService не отдаёт сырой ответ RAWG, а прогоняет его через эвристики:

  • meetsQualityThresholds — отбрасывает игры без названия/картинки, с rating < 3, а также малопопулярные (added < 100 при rating < 4.3).
  • calculateHybridScore — для сортировки по рейтингу учитывает популярность: rating * (1 + log10(added + 1) / 4).
  • getSimilarGames — берёт целевую игру из таблицы games (Supabase), считает «похожесть» по числу общих жанров (+ бонусы за рейтинг/metacritic) и сортирует. Если игры нет в БД — fallback на RAWG (getSimilarGamesFromRawg), затем на getPopularGames.
  • getPopularGames — топ по -added с фильтром rating >= 3.5 и наличием картинки.

Действия пользователя

Вся история действий хранится в таблице user_game_actions, запись идёт через PreferencesService с использованием upsert по уникальному ключу (user_id, game_id, action_type):

Тип (action_type) Метод сервиса
like processGameAction(..., 'like') (снимает конфликтующий dislike)
dislike processGameAction(..., 'dislike') (снимает конфликтующий like)
wishlist processGameAction(..., 'wishlist')
rate processGameRating(...) (поле rating)
status_change updateGameCompletionStatus(...) (поле completion_status)
purchase_change updatePurchaseStatus(...) (поле purchase_status)

Удаление действия — removeGameAction(userId, gameId, actionType). Средняя оценка — getUserAverageRating (усреднение всех записей rate).


🎨 Frontend (Next.js)

Два механизма проксирования

Это самая важная деталь для понимания фронтенда. Запрос с клиента на /api/... обрабатывается одним из двух способов:

  1. Явные Route Handlers (src/app/api/**/route.ts). Они достают JWT из httpOnly-cookie token, подставляют заголовок Authorization: Bearer ... и форвардят запрос на бэкенд. Так работают, например, /api/auth/*, /api/games/:id/like, /api/users/me, /api/preferences/game-actions.

  2. Глобальный rewrite из next.config.ts:

    async rewrites() {
      return [{ source: '/api/:path*', destination: 'http://localhost:3001/:path*' }];
    }

    Rewrite применяется после файловых маршрутов (afterFiles), то есть служит «запасным» прямым proxy на бэкенд для всех /api/*, под которые нет своего route handler.

Из-за этого часть фич ходит мимо явных handler'ов, прямо через rewrite:

Запрос клиента Обработчик Куда уходит на бэкенд
GET /api/games?... route handler games/route.ts GET /games
GET /api/swipes?limit=&exclude= rewrite (своего handler нет) GET /swipes
POST /api/swipes/swipe-action/batch rewrite POST /swipes/swipe-action/batch
GET /api/games/similar/:id rewrite (своего handler нет) GET /games/similar/:id

В этих случаях токен передаётся не из cookie, а явным заголовком Authorization, который клиентский код добавляет сам (он берёт token из AuthContext).

⚠️ В каталоге src/app/api/recommendations/* есть отдельные route handlers (swipes, popular, similar, swipe-action), но реальные страницы их не используют (свайпы ходят на /api/swipes, похожие — на /api/games/similar). Часть из них к тому же ссылается на несуществующие бэкенд-маршруты (/swipes/popular, /swipes/similar, /swipes/swipe-action). Это legacy-код.

Страницы

Маршрут Файл Описание
/ app/page.tsx Главная: hero + подборки (Популярное/Action/RPG/Indie) + блок новостей. Грузит данные параллельно через /api/games?... и /api/news/rss
/games app/games/page.tsxGamesPageContent.tsx Каталог: поиск, фильтры (жанры/платформы/теги/даты), сортировка, пагинация
/games/[id] app/games/[id]/page.tsx Детали игры: описание, скриншоты/трейлеры, кнопки действий, блок похожих игр (/api/games/similar/:id), цены (/api/crop-image для логотипов магазинов)
/swipes app/swipes/page.tsx ⭐ Tinder-свайпы. Требует авторизации (иначе AuthPopup)
/profile app/profile/page.tsx Профиль: шапка, статистика (StatsCharts), сетка игр пользователя, модалка настроек. Защищён middleware.ts
/news app/news/page.tsx Лента новостей с бесконечным скроллом и панелью источников
/auth app/auth/page.tsx Переключение между формами входа/регистрации. Учитывает ?tab= и ?redirect=
* (404) app/not-found.tsx Простая страница «404»

Корневая разметка: layout.tsx подключает шрифты/иконки и рендерит ClientLayout, который оборачивает приложение в AuthProviderGameActionsProvider и выводит боковую навигацию NavigationBlock.

API Routes (proxy layer)

Все маршруты лежат в src/app/api. Сводка:

Auth

Route Метод Логика
/api/auth/login POST Форвардит на /auth/login, при успехе кладёт token (httpOnly) и user в cookie на 7 дней
/api/auth/register POST Аналогично login
/api/auth/logout POST Удаляет cookie token и user (бэкенд не вызывается)
/api/auth/check GET Проверяет cookie + дёргает /auth/validate; есть fallback на cookie user
/api/auth/validate GET Проверяет токен на бэкенде, отдаёт { valid }
/api/auth/sync POST Записывает переданные token/user в cookie

Games / Users / Preferences

Route Методы Куда
/api/games GET /games (форвард query, Cache-Control: no-store)
/api/games/[id] GET /games/:id
/api/games/[id]/like POST, DELETE /games/:id/like (читает cookie-токен)
/api/games/[id]/dislike POST, DELETE /games/:id/dislike
/api/games/[id]/wishlist POST, DELETE /games/:id/wishlist
/api/games/[id]/rate POST, DELETE /games/:id/rate
/api/games/[id]/status POST /games/:id/status
/api/games/[id]/purchase POST /games/:id/purchase
/api/games/genres GET Хардкод-список жанров (RAWG id)
/api/games/platforms GET Список из lib/platforms.ts
/api/users/me GET, PUT /users/me
/api/users/me/games GET /users/me/games
/api/users/me/profile PUT /users/me/profile
/api/preferences/game-actions GET /preferences/game-actions

Внешние интеграции (без бэкенда)

Route Метод Логика
/api/news/rss GET, POST Парсит RSS-ленты на сервере (см. ниже)
/api/deals GET Прокси к plati.io (?q= обязателен), кэш 1 час — цены/предложения
/api/crop-image GET Через sharp авто-обрезает прозрачные края логотипов магазинов с graph.digiseller.com
/api/test Тестовый/служебный endpoint

Компоненты

Компонент Файл Назначение
NavigationBlock NavigationBlock/ Боковое меню, умеет сворачиваться (передаёт onCollapseChange)
GameCard GameCard/ Карточка игры в сетке; подтягивает среднюю цену через getDealsWithAverage
GamesGrid GamesGrid/ Сетка карточек
SwipeCard SwipeCard/ Полноэкранная карта свайпа. Drag через Pointer Events, определение направления left/right/up по порогам смещения, попап с полной инфой
GameActions GameActions/ Кнопки like/dislike/wishlist/rate/status/purchase. Оптимистичные апдейты через useGameActions, откат при ошибке запроса
StarRating StarRating/StarRaing.tsx Интерактивная оценка звёздами, шлёт POST/DELETE /api/games/:id/rate
ScreenshotGallery ScreenshotGallery/ Галерея скриншотов игры
StatsCharts StatsCharts/ Статистика профиля; рисует собственные BarChart/PieChart на SVG/CSS
SearchInput SearchInput/ Поле поиска (debounce)
NewsCard NewsCard/ Карточка новости (варианты medium/tall/wide/large)
RssSourcesPanel RssSourcesPanel/ Управление источниками RSS (вкл/выкл, добавление, удаление)
Skeleton Skeleton/ SkeletonBlock / SkeletonText для состояний загрузки
AuthGuard AuthGuard/ Защита контента: редирект на /auth или показ AuthPopup
AuthPopup AuthPopup/ Модалка «требуется авторизация»
LoginForm / RegisterForm LoginForm/, RegisterForm/ Формы входа/регистрации (используют useAuth)
profile/ProfileHeader profile/ Шапка профиля с аватаром, открытие настроек
profile/ProfileGamesGrid profile/ Сетка игр пользователя с фильтрами по статусам
profile/ProfileGameCard profile/ Карточка игры в профиле
profile/ProfileSettingsModal profile/ Модалка редактирования профиля

Компонента SwipeControls в проекте нет — управление свайпами реализовано жестами внутри SwipeCard и кнопками на самой странице /swipes.

Контексты

AuthContext (contexts/AuthContext.tsx)

Глобальное состояние аутентификации. Предоставляет:

user, profile, token, isAuthenticated, isLoading, login(login, password), register(login, password), logout(), updateUser(), updateProfile(), refreshProfile().

Логика инициализации:

  1. При старте дергает /api/auth/check (серверная проверка через cookie).
  2. Если сервер не подтвердил — fallback на localStorage (token/user/profile).
  3. Данные дублируются в localStorage для мгновенного восстановления сессии.

GameActionsContext (contexts/GameActionsContexts.tsx)

Карта состояний игр (Record<gameId, GameAction>), где GameAction:

type GameAction = {
  liked: boolean;
  disliked: boolean;
  in_wishlist: boolean;
  rating: number | null;
  completion_status: "not_played" | "playing" | "completed" | "dropped";
  purchase_status: "owned" | "not_owned" | "want_to_buy";
};

Особенности:

  • Загрузка действий из /api/preferences/game-actions и раскладка по action_type.
  • Кэш в sessionStorage (ключ game_actions_${token}) на 5 минут.
  • Дедупликация одновременных запросов через pendingRequests: Map.
  • Перезагрузка при смене маршрута с debounce 100 мс.
  • Таймаут запроса 8 секунд (AbortController).
  • setGameAction(gameId, partial) — оптимистичное локальное обновление (его и использует GameActions до подтверждения сервером).

Сервисы, хуки и утилиты

  • services/gameService.ts — основной слой работы с играми на клиенте: gameService() (каталог с фильтрами), getGameById, getGenres, getPlatforms, getDealsWithAverage (цены через /api/deals), searchGames, getPopularGames, getNewGames, а также proxifyImage() — оборачивает URL картинки в внешний image-proxy https://playpulse-rawg-proxy.vercel.app/api/image?url=.... Базовый префикс запросов — API_BASE = "/api".
  • services/scrollService.ts — управление прокруткой.
  • hooks/useMediaActions.ts — работа с медиа (скриншоты/трейлеры) на странице игры.
  • lib/platforms.ts — справочник родительских платформ (PARENT_PLATFORMS_FOR_UI).
  • lib/supabase.ts — клиент Supabase для фронтенда (NEXT_PUBLIC_SUPABASE_URL / NEXT_PUBLIC_SUPABASE_ANON_KEY).

🔐 Аутентификация и JWT

Поток

Регистрация / вход:
  Browser → POST /api/auth/login → (Next.js) → POST /auth/login → (NestJS)
  NestJS: bcrypt.compare(password, hash) → JWT.sign({ sub, login })
  Next.js: кладёт token (httpOnly cookie) + user (cookie), отдаёт { user, token } клиенту
  Client: дублирует token/user в localStorage (AuthContext)

Защищённый запрос:
  Browser → /api/games/:id/like
  Next.js route handler: читает cookie token → добавляет Authorization: Bearer → /games/:id/like
  NestJS: JwtStrategy валидирует токен → пропускает запрос

Выход:
  Browser → POST /api/auth/logout → Next.js удаляет cookie; Client чистит localStorage

Конфигурация JWT

  • Секрет — JWT_SECRET (обязателен; AuthModule бросает ошибку, если не задан).
  • Срок жизни токена — 7d.
  • Payload — { sub: userId, login }.

Guards и стратегия

  • JwtStrategy — извлекает Bearer-токен, проверяет подпись, валидирует пользователя в БД. Имеет in-memory кэш пользователей на 5 минут, чтобы не ходить в Supabase на каждый запрос.
  • JwtAuthGuard (guards/jwt-auth.guardl.ts) — кидает 401 с понятным сообщением.
  • OptionalJwtAuthGuard (guards/optional-jwt.ts) — пропускает и анонимов (возвращает null вместо ошибки).

Контроллеры используют @UseGuards(AuthGuard('jwt')) напрямую (а не кастомный JwtAuthGuard).


🎮 Система свайпов

На клиенте (app/swipes/page.tsx)

Константы:

const BATCH_SIZE = 10;            // сколько игр грузим за раз
const PREFETCH_THRESHOLD = 5;     // за сколько карт до конца подгружаем следующую пачку
const BATCH_FLUSH_INTERVAL = 5000; // как часто отправляем накопленные действия (мс)
const MAX_BATCH_SIZE = 10;        // максимум действий в пачке

Жесты (SwipeCard, Pointer Events):

  • → вправо — Like
  • ← влево — Dislike
  • ↑ вверх — Пропустить (skip)

Логика:

  1. Загрузка пачки: GET /api/swipes?limit=10&exclude=<id,через,запятую> (заголовок Authorization). Уже просмотренные id передаются в exclude.
  2. Лайки/дизлайки копятся в pendingActions и отправляются пачкой:
    • по таймеру каждые 5 секунд / при наборе MAX_BATCH_SIZE;
    • при уходе со страницы — через navigator.sendBeacon('/api/swipes/swipe-action/batch');
    • также есть обработчик visibilitychange.
  3. Prefetch: когда до конца стопки остаётся ≤ 5 карт, грузится следующая пачка.

На сервере (SwipesService.getRandomGamesForSwipes)

  1. Берёт из user_game_actions все игры, с которыми пользователь уже взаимодействовал, и объединяет с exclude.
  2. Делает 3 параллельных запроса к случайным страницам (1–10) RAWG, сортировка -rating.
  3. Фильтрует: rating >= 4.0, есть background_image, не в списке просмотренных.
  4. Если игр не хватило — ещё 2 случайные страницы (11–15).
  5. Если совсем пусто — fallback: любые популярные с картинкой (-added).
  6. Перемешивает и возвращает limit игр.

При ошибке возвращается пустой массив — UI не ломается.


📰 Лента новостей (RSS)

Реализована полностью на стороне Next.js (/api/news/rss + api/news/rss/utils.ts):

  • GET /api/news/rss — принимает ?sources=id1,id2&limit=50, по умолчанию берёт включённые источники из DEFAULT_SOURCES, тянет ленты параллельно, фильтрует, сортирует по дате, перемешивает и режет по limit.
  • POST /api/news/rss — принимает { sources: [...] } (используется страницей новостей, где источники хранятся в localStorage).
  • Парсинг — через @xmldom/xmldom. Поддерживаются RSS и Atom: title, link, description/summary, content:encoded, дата, картинка (media:thumbnail, media:content, enclosure, <img> в контенте), категория, guid.
  • Обход блокировок — сначала прямой запрос с «браузерным» User-Agent, при неудаче цепочка CORS-прокси (allorigins, corsproxy.io, cors.bridged.cc). Таймаут 15 с.

Источники по умолчанию (DEFAULT_SOURCES): StopGame, Cubiq, GoHa.Ru, iGuides, DTF.

На странице /news: бесконечный скролл (IntersectionObserver), панель источников (RssSourcesPanel) с сохранением выбора в localStorage, скелетоны при загрузке.


🗄 База данных (Supabase)

Проект использует Supabase как PostgreSQL. Подключение — через серверный ключ (SUPABASE_SERVICE_KEY, иначе SUPABASE_KEY). При старте SupabaseService проверяет соединение запросом к user_game_actions.

⚠️ В репозитории нет SQL-миграций. Схема ниже восстановлена по коду (полям, которые читаются/пишутся сервисами) и приведена как ориентир, а не как точный DDL. Реальные типы/ограничения настроены в самом Supabase.

Таблицы, к которым обращается код:

users

Используемые поля: id, login, password_hash, created_at.

user_profiles

Используемые поля: user_id, name, avatar_url, bio, preferred_language, total_likes, total_dislikes, total_games_added (в коде также объявлены favorite_genres, favorite_tags).

user_game_actions

Главная таблица взаимодействий. Поля, которые пишет/читает PreferencesService:

user_id, game_id, game_name, game_image,
action_type,          -- like | dislike | wishlist | rate | status_change | purchase_change
rating,               -- для action_type = 'rate'
genres (jsonb), tags (jsonb),
completion_status,    -- not_played | playing | completed | dropped
purchase_status,      -- owned | not_owned | want_to_buy
created_at

Upsert идёт по уникальному ключу (user_id, game_id, action_type) — значит, для корректной работы в Supabase должен быть соответствующий unique constraint.

games

Кэш-таблица данных RAWG, читается в getSimilarGames / getCachedGameById / searchCachedGames. Поля: rawg_id, name, slug, released, background_image, rating, metacritic, genres, tags, screenshots, added.


🚀 Кэширование (Redis)

  • Подключение настраивается в app.module.ts через CacheModule + cache-manager-redis-yet, URL из REDIS_URL (по умолчанию redis://localhost:6379).
  • Кэшируемые эндпоинты бэкенда:
Endpoint TTL
GET /games 300 c
GET /games/:id 3600 c
GET /preferences/game-actions 60 c
  • На фронтенде дополнительно кэшируются действия пользователя в sessionStorage (5 минут) и список жанров/платформ — через cache: "force-cache" в gameService.

🔑 Переменные окружения

Backend (apps/backend/.env)

Переменная Обяз. Назначение
SUPABASE_URL URL проекта Supabase
SUPABASE_KEY Anon/public ключ
SUPABASE_SERVICE_KEY Service-role ключ (если задан — используется вместо SUPABASE_KEY)
JWT_SECRET Секрет для подписи JWT (без него приложение не стартует)
REDIS_URL URL Redis (по умолчанию redis://localhost:6379)
PORT Порт API (по умолчанию 3001)
FRONTEND_URL Origin для CORS (по умолчанию http://localhost:3000)

Frontend (apps/frontend/.env.local)

Переменная Назначение
NEXT_PUBLIC_API_URL URL бэкенда для route handlers (по умолчанию http://localhost:3001)
NEXT_PUBLIC_BACKEND_URL То же, используется в /api/users/*
BACKEND_URL Серверный URL бэкенда для /api/games
NEXT_PUBLIC_SUPABASE_URL URL Supabase (для next/image и клиента)
NEXT_PUBLIC_SUPABASE_ANON_KEY Anon-ключ Supabase для фронтенда

⚠️ Файлов .env.example в репозитории нет (хотя make env-setup и docker-compose на них ссылаются). Создайте .env/.env.local вручную по таблицам выше. Хост из NEXT_PUBLIC_API_URL (по умолчанию localhost:3001) должен совпадать с адресом из rewrite в next.config.ts.


▶️ Запуск проекта

Требования

  • Node.js 18+
  • Redis (локально или в облаке)
  • Аккаунт Supabase с таблицами из раздела выше
  • Доступ к RAWG proxy (используется публичный на Vercel)

Вариант 1 — локально

# Установка всех зависимостей (корень + backend + frontend)
make install

# Создать backend .env (нужен существующий .env.example — см. примечание выше)
# и заполнить переменные backend/frontend

# Запуск Redis + frontend + backend разом
make run

make run проверит и при необходимости поднимет Redis, затем запустит frontend и backend через concurrently.

Вручную (в двух терминалах):

# Терминал 1 — backend
cd apps/backend && npm run start:dev

# Терминал 2 — frontend
cd apps/frontend && npm run dev

Вариант 2 — Docker

make run-docker      # docker-compose up -d (redis + backend + frontend)
make logs            # логи всех сервисов
make stop-docker     # остановить

Проверка


🧰 Make-команды

Команда Действие
make install Установка зависимостей (root + backend + frontend)
make run Проверка/запуск Redis + frontend + backend (concurrently)
make tunnel localtunnel на порт 3000 (поддомен play-pulse)
make run-docker / make stop-docker / make restart-docker Управление docker-compose
make logs / make logs-service service=<backend|frontend|redis> Логи
make redis-start / make redis-stop Локальный Redis
make build Сборка backend + frontend
make clean Удаление dist, .next, node_modules
make test / make test-e2e Тесты backend (frontend-тесты не настроены)
make lint / make format Линт/формат backend
make env-setup Копирование apps/backend/.env.example.env
make health-check Пинг backend/frontend/redis

Скрипты npm:

  • backend: start:dev, start:prod, build, test, test:e2e, lint, format.
  • frontend: dev, build, start, lint.

⚠️ Известные нюансы и техдолг

Зафиксировано по состоянию кода, чтобы документация не вводила в заблуждение:

  1. Swagger не поднят. В main.ts есть console.log про доступность Swagger на /api, и в контроллерах расставлены декораторы @Api*, но SwaggerModule.setup() нигде не вызывается — реального UI документации по /api нет.
  2. POST /auth/logout на бэкенде есть, но это «пустышка» (возвращает сообщение, токен не инвалидирует). Фронтенд для выхода его не вызывает — он просто чистит cookie через свой /api/auth/logout.
  3. /api/recommendations/* — legacy route handlers, не используемые страницами; часть ссылается на несуществующие бэкенд-маршруты (/swipes/popular, /swipes/similar, /swipes/swipe-action). Похожие/популярные на бэкенде живут под /games/similar/:id и /games/popular.
  4. /api/swipes и /api/games/similar не имеют своих route handlers и работают через rewrite из next.config.ts напрямую на бэкенд.
  5. Нет .env.example (см. раздел про переменные окружения).
  6. Нет SQL-миграций — схема БД восстановлена по коду и должна быть создана в Supabase вручную (включая unique-ключ user_game_actions(user_id, game_id, action_type)).
  7. /api/games/genres отдаёт захардкоженный список жанров, а не данные RAWG.
  8. Зависимости recharts/zustand присутствуют в package.json; графики профиля при этом нарисованы кастомно на SVG/CSS внутри StatsCharts.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages