This commit is contained in:
2026-02-06 11:52:30 +03:00
parent 1812de0baf
commit fd7bf76abd
4 changed files with 289 additions and 6 deletions

View File

@ -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<string>('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,

View File

@ -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<Response>();
const request = ctx.getRequest<Request>();
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,
});
}
}

View File

@ -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());

224
situation.md Normal file
View File

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