From fd7bf76abd92f84cc71137d1ec3bb354005c5e84 Mon Sep 17 00:00:00 2001 From: Bonchellon Date: Fri, 6 Feb 2026 11:52:30 +0300 Subject: [PATCH] 123 --- apps/api/src/auth/auth.module.ts | 20 +- apps/api/src/common/all-exceptions.filter.ts | 48 ++++ apps/api/src/main.ts | 3 + situation.md | 224 +++++++++++++++++++ 4 files changed, 289 insertions(+), 6 deletions(-) create mode 100644 apps/api/src/common/all-exceptions.filter.ts create mode 100644 situation.md diff --git a/apps/api/src/auth/auth.module.ts b/apps/api/src/auth/auth.module.ts index 85092d4..00721d6 100644 --- a/apps/api/src/auth/auth.module.ts +++ b/apps/api/src/auth/auth.module.ts @@ -15,12 +15,20 @@ import { UsersModule } from '../users/users.module'; PassportModule.register({ defaultStrategy: 'jwt' }), JwtModule.registerAsync({ imports: [ConfigModule], - useFactory: async (configService: ConfigService) => ({ - secret: configService.get('JWT_SECRET'), - signOptions: { - expiresIn: '7d', - }, - }), + useFactory: async (configService: ConfigService) => { + const secret = configService.get('JWT_SECRET'); + if (!secret || secret.trim() === '') { + throw new Error( + 'JWT_SECRET is not set or empty. Set it in .env and run Docker with: docker compose --env-file .env up -d', + ); + } + return { + secret: secret.trim(), + signOptions: { + expiresIn: '7d', + }, + }; + }, inject: [ConfigService], }), UsersModule, diff --git a/apps/api/src/common/all-exceptions.filter.ts b/apps/api/src/common/all-exceptions.filter.ts new file mode 100644 index 0000000..f0d527f --- /dev/null +++ b/apps/api/src/common/all-exceptions.filter.ts @@ -0,0 +1,48 @@ +import { + ExceptionFilter, + Catch, + ArgumentsHost, + HttpException, + HttpStatus, + Logger, +} from '@nestjs/common'; +import { Request, Response } from 'express'; + +/** + * Логирует все необработанные ошибки в консоль (видны в docker logs coursecraft-api). + * Помогает диагностировать 500 при запуске в Docker. + */ +@Catch() +export class AllExceptionsFilter implements ExceptionFilter { + private readonly logger = new Logger(AllExceptionsFilter.name); + + catch(exception: unknown, host: ArgumentsHost): void { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + const request = ctx.getRequest(); + + const status = + exception instanceof HttpException + ? exception.getStatus() + : HttpStatus.INTERNAL_SERVER_ERROR; + + const message = + exception instanceof HttpException + ? exception.getResponse() + : exception instanceof Error + ? exception.message + : 'Internal server error'; + + this.logger.error( + `${request.method} ${request.url} → ${status}`, + exception instanceof Error ? exception.stack : String(exception), + ); + + response.status(status).json({ + statusCode: status, + message: typeof message === 'object' && message && 'message' in message + ? (message as { message: string }).message + : message, + }); + } +} diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index 83231b4..6a581d8 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -4,11 +4,14 @@ import { ConfigService } from '@nestjs/config'; import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; import helmet from 'helmet'; import { AppModule } from './app.module'; +import { AllExceptionsFilter } from './common/all-exceptions.filter'; async function bootstrap() { const app = await NestFactory.create(AppModule); const configService = app.get(ConfigService); + app.useGlobalFilters(new AllExceptionsFilter()); + // Security app.use(helmet()); diff --git a/situation.md b/situation.md new file mode 100644 index 0000000..ae9eedf --- /dev/null +++ b/situation.md @@ -0,0 +1,224 @@ +# Текущая ситуация проекта CourseCraft + +Документ описывает состояние репозитория и способы запуска (на хосте и в Docker). + +--- + +## Состояние проекта + +### Монорепозиторий (pnpm workspaces + Turbo) + +- **apps/api** — NestJS API (авторизация, курсы, генерация, платежи, поиск). Порт по умолчанию: **3125**. +- **apps/web** — Next.js 14 (App Router). Порт по умолчанию: **3080**. +- **apps/ai-service** — воркер AI-пайплайна (OpenRouter, BullMQ). Без своего HTTP-порта, подключается к Redis. +- **packages/database** — Prisma-схема и клиент. Для `prisma db push` нужен свой `.env` в `packages/database/` с `DATABASE_URL`. +- **packages/shared** — общие типы и константы. + +### Порты + +| Сервис | Порт | Описание | +|---------------|-------|-----------------------------| +| API | 3125 | Бэкенд, Swagger на /api/docs | +| Web | 3080 | Фронтенд | +| Postgres | 5432 | БД | +| Redis | 6395 | Очереди (на хосте 6395→6379)| +| Meilisearch | 7700 | Поиск | + +### Переменные окружения + +Критичные для работы: + +- **Supabase:** `NEXT_PUBLIC_SUPABASE_URL`, `NEXT_PUBLIC_SUPABASE_ANON_KEY`, `SUPABASE_SERVICE_ROLE_KEY` +- **JWT:** `JWT_SECRET` (подпись токенов после обмена с Supabase) +- **БД:** `DATABASE_URL` (PostgreSQL) +- **Redis:** `REDIS_URL`, `REDIS_HOST`, `REDIS_PORT` +- **OpenRouter:** `OPENROUTER_API_KEY` +- **Meilisearch:** `MEILISEARCH_HOST`, `MEILISEARCH_API_KEY` + +Корневой `.env` используется при запуске приложений на хосте и при `docker compose` (см. ниже). + +--- + +## Запуск: два варианта + +### 1. Всё в Docker (рекомендуется для сервера) + +Вся инфраструктура и приложения в контейнерах; после отключения от сервера процессы не падают. + +**Важно:** Compose и `.env` должны быть в **корне** проекта. Используется корневой `docker-compose.yml` (не `docker/docker-compose.yml`). + +**Шаги:** + +```bash +cd /usr/local/course-craft-service # или путь к репозиторию +``` + +Убедиться, что в корне есть `.env` с нужными ключами (Supabase, JWT_SECRET и т.д.). + +```bash +docker compose --env-file .env up -d --build +``` + +Или через pnpm (то же самое): + +```bash +pnpm docker:up +``` + +После первого запуска таблицы в БД создаются при старте API (entrypoint делает `prisma db push`). Данные БД/Redis/Meilisearch лежат в `docker/data/`. + +**Проверка:** + +- Web: `http://<хост>:3080` +- API: `http://<хост>:3125/api` +- Переменные в контейнере API: + `docker exec coursecraft-api env | grep -E "JWT_SECRET|SUPABASE"` + значения не должны быть пустыми. + +**Остановка:** + +```bash +docker compose down +``` + +**Логи:** + +```bash +docker compose logs -f +# или по сервису +docker logs coursecraft-api -f +``` + +--- + +### 2. Только инфраструктура в Docker, приложения на хосте + +Удобно для разработки: БД, Redis, Meilisearch в Docker; API, Web и AI-service — через pnpm. + +**Шаги:** + +1. Поднять только инфраструктуру (если используется корневой compose): + + ```bash + cd /usr/local/course-craft-service + docker compose --env-file .env up -d postgres redis meilisearch + ``` + + Либо старый вариант только инфраструктуры из `docker/` (без api, web, ai-service). + +2. Применить схему БД (один раз или после изменений в Prisma): + + В корне должен быть `.env` с `DATABASE_URL`. Для Prisma также нужен `packages/database/.env`: + + ```bash + echo 'DATABASE_URL="postgresql://postgres:postgres@localhost:5432/coursecraft?schema=public"' > packages/database/.env + pnpm db:push + ``` + +3. Запуск приложений: + + ```bash + pnpm start + ``` + + Или в режиме разработки: + + ```bash + pnpm dev + ``` + +Доступ: Web — 3080, API — 3125 (или как задано в `.env`: PORT, скрипты next). + +--- + +## Скрипт run.sh (деплой на сервере) + +`run.sh` в корне репозитория: + +1. Останавливает Docker Compose (старый путь `docker/docker-compose.yml`). +2. Делает `git pull`. +3. Выполняет `pnpm install` и `pnpm build`. +4. Поднимает Docker Compose. +5. В фоне запускает `pnpm start` (API, Web, AI-service на хосте). + +То есть он смешивает: инфраструктура через Docker, приложения — процессы на хосте. Для «всё в Docker» лучше использовать только `docker compose --env-file .env up -d` из корня (или `pnpm docker:up`), без шага с `pnpm start`. + +--- + +## Важные моменты + +1. **Корневой `.env`** + При запуске `docker compose` из корня с `--env-file .env` переменные подставляются в `docker-compose.yml` и передаются в контейнеры. Без этого JWT и Supabase в API будут пустыми → 500 на `/api/auth/exchange` и т.п. + +2. **Откуда запускать Compose** + Запуск только из **корня проекта** (где лежит `.env` и `docker-compose.yml`). Не из папки `docker/`. + +3. **Prisma в контейнере API** + Используется образ на базе `node:20-slim` (не Alpine), чтобы не было ошибок с `libssl.so.1.1` у нативного движка Prisma. + +4. **Прокси API во фронте** + Web обращается к API по относительному пути `/api/...`; Next.js проксирует запросы на бэкенд (в Docker — на сервис `api:3125`). Один билд веба работает при любом хосте. + +5. **Два compose-файла** + - **Корень:** `docker-compose.yml` — основной, с `env_file: .env` и путями от корня. Использовать его для полного запуска в Docker. + - **docker/docker-compose.yml** — старый вариант (можно оставить для только инфраструктуры или выровнять с корневым при необходимости). + +--- + +## Если в Docker 500 на `/api/auth/exchange` и `/api/courses` (а через pnpm run — всё ок) + +Чаще всего API в контейнере не получает переменные окружения или получает их с ошибкой (например, из-за Windows). + +**1. Запуск обязательно с `--env-file .env` из корня проекта:** + +```bash +cd d:\Github\course-craft-service +docker compose --env-file .env up -d --build +``` + +Или через pnpm: `pnpm docker:up` (если скрипт использует `--env-file .env`). + +**2. Проверка переменных в контейнере API:** + +```bash +docker exec coursecraft-api env | findstr "JWT_SECRET SUPABASE DATABASE" +``` + +Должны быть непустые `JWT_SECRET`, `NEXT_PUBLIC_SUPABASE_URL`, `SUPABASE_SERVICE_ROLE_KEY`, `DATABASE_URL=postgresql://postgres:postgres@postgres:5432/...`. Если что-то пустое — Compose не подхватил `.env` (запуск не из корня или не указан `--env-file .env`). + +**3. Windows: перевод строк в `.env`** + +Если `.env` сохранён с CRLF (Windows), значения могут содержать `\r` и ломать JWT/подключения. Сохраните `.env` в кодировке UTF-8 с окончаниями строк LF (в редакторе: «Save with Encoding» → UTF-8, в Git: `git config core.autocrlf input` и пересохраните файл). + +**4. Логи API при 500:** + +После правок при следующей ошибке API при старте покажет явную причину, если не задан `JWT_SECRET`. Для любых необработанных ошибок смотрите логи: + +```bash +docker logs coursecraft-api -f +``` + +Перед запросом к `/api/auth/exchange` или `/api/courses` в логах будет стек или сообщение об ошибке (БД, Supabase, JWT и т.д.). + +**5. Web должен проксировать на контейнер API** + +В контейнере `web` при старте должна быть переменная `API_URL=http://api:3125` (задаётся в `docker-compose.yml`). Тогда Next.js отправляет запросы на сервис `api`, а не на `localhost`. Проверка: + +```bash +docker exec coursecraft-web env | findstr API_URL +``` + +Ожидается: `API_URL=http://api:3125`. + +--- + +## Краткая шпаргалка + +| Задача | Команда | +|---------------------|--------| +| Всё в Docker | `cd <корень> && docker compose --env-file .env up -d` или `pnpm docker:up` | +| Только инфраструктура | `docker compose --env-file .env up -d postgres redis meilisearch` | +| Схема БД (на хосте) | `packages/database/.env` с DATABASE_URL, затем `pnpm db:push` | +| Приложения на хосте | `pnpm start` или `pnpm dev` | +| Логи Docker | `docker compose logs -f` или `docker logs coursecraft-api -f` | +| Остановить Docker | `docker compose down` |