123
This commit is contained in:
@ -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'),
|
||||
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,
|
||||
|
||||
48
apps/api/src/common/all-exceptions.filter.ts
Normal file
48
apps/api/src/common/all-exceptions.filter.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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
224
situation.md
Normal 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` |
|
||||
Reference in New Issue
Block a user