Adapt clean-pdf plan to staging-based verification (no local Docker)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,7 @@
|
|||||||
- Лимиты: `PDF_MONTHLY_LIMIT` (default 100), burst 5/мин, browserless `CONCURRENT=2`.
|
- Лимиты: `PDF_MONTHLY_LIMIT` (default 100), burst 5/мин, browserless `CONCURRENT=2`.
|
||||||
- Проверки перед каждым коммитом: `npm run lint && npm run type-check && npm run test`.
|
- Проверки перед каждым коммитом: `npm run lint && npm run type-check && npm run test`.
|
||||||
- Работать в ветке `feature/clean-pdf`.
|
- Работать в ветке `feature/clean-pdf`.
|
||||||
|
- **Окружение:** на Mac НЕТ Docker. Контейнеры (postgres, browserless) живут на staging-сервере Hetzner: `root@178.104.27.196`, стек `/root/digital-household/lms-staging/` (docker-compose: `lms-staging-app-1` порт 3011 → https://staging.school.second-brain.ru, `lms-staging-db-1`). Сборочный клон репо на сервере: `/root/lms-staging-build/`. Деплой staging: `bash ~/Documents/Claude/scripts/deploy-staging.sh` (перед этим в `/root/lms-staging-build` должна быть выкачана нужная ветка). Миграции применяет entrypoint контейнера (`prisma migrate deploy`) при старте. Юнит-тесты, lint, type-check, build — локально на Mac; доступ к browserless с Mac — через SSH-туннель `ssh -f -N -L 3333:localhost:3333 root@178.104.27.196`. Все шаги плана с `docker compose ...` локально НЕ выполняются — их staging-эквиваленты прописаны в задачах.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -51,7 +52,7 @@ npm install defuddle jsdom playwright-core && npm install -D @types/jsdom
|
|||||||
|
|
||||||
Expected: package.json/package-lock.json обновлены, без ошибок peer-deps.
|
Expected: package.json/package-lock.json обновлены, без ошибок peer-deps.
|
||||||
|
|
||||||
- [ ] **Step 3: browserless в dev-compose**
|
- [ ] **Step 3: browserless в dev-compose (на будущее, локально не проверяется)**
|
||||||
|
|
||||||
В `docker-compose.yml` добавить сервис (после `db`, до `volumes`):
|
В `docker-compose.yml` добавить сервис (после `db`, до `volumes`):
|
||||||
|
|
||||||
@@ -67,13 +68,24 @@ Expected: package.json/package-lock.json обновлены, без ошибок
|
|||||||
TIMEOUT: "120000"
|
TIMEOUT: "120000"
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 4: Поднять и проверить browserless**
|
На Mac Docker нет — этот файл правим для будущей локальной разработки, НЕ поднимаем.
|
||||||
|
|
||||||
```bash
|
- [ ] **Step 4: browserless в staging-стек на Hetzner**
|
||||||
docker compose up -d browserless && sleep 5 && curl -s -o /dev/null -w "%{http_code}\n" http://localhost:3333/docs
|
|
||||||
|
На сервере в `/root/digital-household/lms-staging/docker-compose.yml` добавить сервис `browserless` (тот же блок, что в Step 3 — с `ports: "127.0.0.1:3333:3000"`, порт нужен для SSH-туннеля с Mac), а в сервис `app` — `depends_on: browserless: condition: service_started` не добавлять (staging app перезапускается деплой-скриптом, зависимость не обязательна). В `/root/digital-household/lms-staging/.env` добавить:
|
||||||
|
|
||||||
|
```
|
||||||
|
BROWSER_WS_URL=ws://browserless:3000
|
||||||
|
PDF_MONTHLY_LIMIT=100
|
||||||
```
|
```
|
||||||
|
|
||||||
Expected: `200`. Если имена env-переменных не подхватились (проверка: `docker compose logs browserless | head -30` — не должно быть предупреждений о неизвестных переменных), свериться с `http://localhost:3333/docs` и поправить.
|
Правку compose на сервере делать так: скачать файл (`scp`), поправить локально, залить обратно — НЕ heredoc в ssh. Затем:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh root@178.104.27.196 "cd /root/digital-household/lms-staging && docker compose up -d browserless && sleep 5 && curl -s -o /dev/null -w '%{http_code}\n' http://localhost:3333/docs"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `200`. Проверить логи: `ssh root@178.104.27.196 "docker logs lms-staging-browserless-1 --tail 20"` — без ошибок конфигурации.
|
||||||
|
|
||||||
- [ ] **Step 5: serverExternalPackages в next.config.ts**
|
- [ ] **Step 5: serverExternalPackages в next.config.ts**
|
||||||
|
|
||||||
@@ -91,7 +103,7 @@ include: ["src/lib/**/__tests__/**/*.test.ts"],
|
|||||||
|
|
||||||
- [ ] **Step 7: env для dev**
|
- [ ] **Step 7: env для dev**
|
||||||
|
|
||||||
В `.env.local` добавить:
|
В `.env.local` добавить (browserless доступен с Mac через SSH-туннель: `ssh -f -N -L 3333:localhost:3333 root@178.104.27.196`):
|
||||||
|
|
||||||
```
|
```
|
||||||
BROWSER_WS_URL=ws://localhost:3333
|
BROWSER_WS_URL=ws://localhost:3333
|
||||||
@@ -711,13 +723,36 @@ model PdfApiKey {
|
|||||||
pdfApiKey PdfApiKey?
|
pdfApiKey PdfApiKey?
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 2: Прогнать миграцию на dev-базе**
|
- [ ] **Step 2: Написать миграцию вручную и перегенерировать клиент**
|
||||||
|
|
||||||
```bash
|
Локальной БД нет (`prisma migrate dev` невозможен) — миграцию пишем руками по образцу существующих (`prisma/migrations/20260623120000_add_tool_usage/migration.sql`), применит её `prisma migrate deploy` в entrypoint при деплое staging/prod.
|
||||||
docker compose up -d db && npx prisma migrate dev --name add_pdf_api_key
|
|
||||||
|
Создать `prisma/migrations/20260706120000_add_pdf_api_key/migration.sql`:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Чистый PDF — персональный API-ключ студента (Zotero/curl)
|
||||||
|
CREATE TABLE "PdfApiKey" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"key" TEXT NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
CONSTRAINT "PdfApiKey_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX "PdfApiKey_userId_key" ON "PdfApiKey"("userId");
|
||||||
|
CREATE UNIQUE INDEX "PdfApiKey_key_key" ON "PdfApiKey"("key");
|
||||||
|
|
||||||
|
ALTER TABLE "PdfApiKey" ADD CONSTRAINT "PdfApiKey_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
```
|
```
|
||||||
|
|
||||||
Expected: миграция создана и применена, Prisma Client перегенерирован (`src/generated/prisma/`).
|
Затем перегенерировать клиент (БД не нужна):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx prisma generate
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: клиент в `src/generated/prisma/` обновился, `prisma.pdfApiKey` доступен в типах.
|
||||||
|
|
||||||
- [ ] **Step 3: Написать keys.ts**
|
- [ ] **Step 3: Написать keys.ts**
|
||||||
|
|
||||||
@@ -1060,14 +1095,14 @@ describe.runIf(enabled)("generateCleanPdf (integration, нужен browserless)"
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 3: Run integration test**
|
- [ ] **Step 3: Run integration test (browserless на staging через туннель)**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose up -d browserless
|
ssh -f -N -L 3333:localhost:3333 root@178.104.27.196 2>/dev/null || true
|
||||||
RUN_PDF_INTEGRATION=1 BROWSER_WS_URL=ws://localhost:3333 npx vitest run src/lib/clean-pdf/__tests__/generate.int.test.ts
|
RUN_PDF_INTEGRATION=1 BROWSER_WS_URL=ws://localhost:3333 npx vitest run src/lib/clean-pdf/__tests__/generate.int.test.ts
|
||||||
```
|
```
|
||||||
|
|
||||||
Expected: PASS (~10–30 сек). Если URL статьи умер — заменить на любую живую статью с habr.com в тесте.
|
Expected: PASS (~10–40 сек с учётом туннеля). Если URL статьи умер — заменить на любую живую статью с habr.com в тесте. Если туннель не поднялся (`connection refused`) — проверить, что browserless на staging запущен (Task 1 Step 4).
|
||||||
|
|
||||||
- [ ] **Step 4: Сохранить образец глазами**
|
- [ ] **Step 4: Сохранить образец глазами**
|
||||||
|
|
||||||
@@ -1204,35 +1239,42 @@ npm run lint && npm run type-check
|
|||||||
|
|
||||||
Expected: PASS.
|
Expected: PASS.
|
||||||
|
|
||||||
- [ ] **Step 4: Живая проверка через dev-сервер**
|
- [ ] **Step 4: Живая проверка на staging**
|
||||||
|
|
||||||
|
Запушить ветку и задеплоить её на staging:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose up -d db browserless
|
git push -u origin feature/clean-pdf
|
||||||
npm run dev &
|
ssh root@178.104.27.196 "cd /root/lms-staging-build && git fetch && git checkout feature/clean-pdf && git pull origin feature/clean-pdf"
|
||||||
sleep 8
|
bash ~/Documents/Claude/scripts/deploy-staging.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: деплой зелёный (`✅ Staging OK`); в логах старта `Running database migrations...` без ошибок — миграция `add_pdf_api_key` применилась.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
BASE=https://staging.school.second-brain.ru
|
||||||
# 1) Без авторизации → 401
|
# 1) Без авторизации → 401
|
||||||
curl -s -o /dev/null -w "%{http_code}\n" "http://localhost:3000/api/pdf?url=https://example.com"
|
curl -s -o /dev/null -w "%{http_code}\n" "$BASE/api/pdf?url=https://example.com"
|
||||||
# 2) С мусорным ключом → 401
|
# 2) С мусорным ключом → 401
|
||||||
curl -s -H "Authorization: Bearer sbpdf_wrong" -o /dev/null -w "%{http_code}\n" "http://localhost:3000/api/pdf?url=https://example.com"
|
curl -s -H "Authorization: Bearer sbpdf_wrong" -o /dev/null -w "%{http_code}\n" "$BASE/api/pdf?url=https://example.com"
|
||||||
```
|
```
|
||||||
|
|
||||||
Expected: `401` и `401`, оба JSON. Затем взять реальный ключ (после Task 5 создать вручную:
|
Expected: `401` и `401`, оба JSON. Затем вставить тестовый ключ админу в staging-БД:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose exec db psql -U lms_user -d lms_db -c \
|
ssh root@178.104.27.196 "docker exec -i lms-staging-db-1 psql -U lms_staging_user -d lms_staging_db -c \"INSERT INTO \\\"PdfApiKey\\\" (id, \\\"userId\\\", key, \\\"createdAt\\\", \\\"updatedAt\\\") SELECT 'pdfkey-test-1', id, 'sbpdf_devtest123456789012345678901234', now(), now() FROM \\\"User\\\" WHERE role='admin' LIMIT 1;\""
|
||||||
"INSERT INTO \"PdfApiKey\" (id, \"userId\", key, \"createdAt\", \"updatedAt\") SELECT 'pdfkey-test-1', id, 'sbpdf_devtest123456789012345678901234', now(), now() FROM \"User\" WHERE role='admin' LIMIT 1;"
|
|
||||||
curl -s -D - -H "Authorization: Bearer sbpdf_devtest123456789012345678901234" \
|
curl -s -D - -H "Authorization: Bearer sbpdf_devtest123456789012345678901234" \
|
||||||
"http://localhost:3000/api/pdf?url=https://habr.com/ru/articles/942236/" -o /tmp/api-test.pdf | head -15
|
"$BASE/api/pdf?url=https://habr.com/ru/articles/942236/" -o /tmp/api-test.pdf | head -15
|
||||||
```
|
```
|
||||||
|
|
||||||
Expected: `200`, `content-type: application/pdf`, заголовки `X-Uses-Count: 1`, `X-Max-Uses: 100`; `/tmp/api-test.pdf` открывается. Проверить SSRF:
|
Expected: `200`, `content-type: application/pdf`, заголовки `X-Uses-Count: 1`, `X-Max-Uses: 100`; `/tmp/api-test.pdf` открывается (`file /tmp/api-test.pdf` → PDF). Если экранирование psql через ssh мешает — положить SQL в файл и передать через stdin: `ssh root@178.104.27.196 "docker exec -i lms-staging-db-1 psql -U lms_staging_user -d lms_staging_db" < /tmp/insert-key.sql`. Проверить SSRF:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -s -H "Authorization: Bearer sbpdf_devtest123456789012345678901234" "http://localhost:3000/api/pdf?url=http://192.168.1.1/" ; echo
|
curl -s -H "Authorization: Bearer sbpdf_devtest123456789012345678901234" "$BASE/api/pdf?url=http://192.168.1.1/" ; echo
|
||||||
curl -s -H "Authorization: Bearer sbpdf_devtest123456789012345678901234" "http://localhost:3000/api/pdf?url=http://localhost:5432/"; echo
|
curl -s -H "Authorization: Bearer sbpdf_devtest123456789012345678901234" "$BASE/api/pdf?url=http://localhost:5432/"; echo
|
||||||
```
|
```
|
||||||
|
|
||||||
Expected: оба `{"error":"Адрес недоступен"}` (422). Остановить dev-сервер.
|
Expected: оба `{"error":"Адрес недоступен"}` (422).
|
||||||
|
|
||||||
- [ ] **Step 5: Commit**
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
@@ -1530,13 +1572,11 @@ npm run lint && npm run type-check && npm run test
|
|||||||
|
|
||||||
Expected: PASS.
|
Expected: PASS.
|
||||||
|
|
||||||
- [ ] **Step 6: Живая проверка в браузере**
|
- [ ] **Step 6: Живая проверка в браузере (staging)**
|
||||||
|
|
||||||
```bash
|
Задеплоить текущее состояние ветки на staging (как в Task 8 Step 4: push → checkout ветки в `/root/lms-staging-build` → `deploy-staging.sh`).
|
||||||
docker compose up -d db browserless && npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
Вручную (или через agent-browser в `--headed`): войти студентом с платным курсом → /tools → карточка «Чистый PDF» видна → открыть → сгенерировать PDF реальной статьи → файл скачался, счётчик увеличился → ключ показать/скопировать/перевыпустить (после перевыпуска старый ключ по curl даёт 401) → у аккаунта только с бесплатным курсом страница показывает заглушку, а /api/pdf отдаёт 403.
|
Через agent-browser в `--headed` (или вручную) на `https://staging.school.second-brain.ru`: войти админом (креды — memory `reference_lms_admin_credentials.md`; staging-БД содержит копию пользователей) → /tools → карточка «Чистый PDF» видна → открыть → сгенерировать PDF реальной статьи → файл скачался, счётчик увеличился → ключ показать/скопировать/перевыпустить (после перевыпуска старый ключ по curl даёт 401). Проверка заглушки для бесплатного аккаунта: найти в staging-БД пользователя без платных enrollment (или временно создать) и убедиться, что страница показывает заглушку, а /api/pdf с его сессией отдаёт 403.
|
||||||
|
|
||||||
- [ ] **Step 7: Commit**
|
- [ ] **Step 7: Commit**
|
||||||
|
|
||||||
@@ -1609,6 +1649,8 @@ git commit -m "Add browserless service to prod compose and document clean-pdf"
|
|||||||
|
|
||||||
## Деплой-заметки (выполняются при релизе, не частью этого плана)
|
## Деплой-заметки (выполняются при релизе, не частью этого плана)
|
||||||
|
|
||||||
|
0. Staging уже получил browserless и env в Task 1 — на релизе речь только о проде (Hoster.kz).
|
||||||
|
|
||||||
1. В `.env` на Hoster.kz добавить `BROWSERLESS_TOKEN=<random>` (например `openssl rand -hex 24`) и при желании `PDF_MONTHLY_LIMIT`.
|
1. В `.env` на Hoster.kz добавить `BROWSERLESS_TOKEN=<random>` (например `openssl rand -hex 24`) и при желании `PDF_MONTHLY_LIMIT`.
|
||||||
2. Миграция на проде: выполняется как обычно при деплое (`prisma migrate deploy` в entrypoint — проверить, что `add_pdf_api_key` применилась: `docker exec -i lms-sb-db-1 psql -U lms_user -d lms_db -c '\d "PdfApiKey"'`).
|
2. Миграция на проде: выполняется как обычно при деплое (`prisma migrate deploy` в entrypoint — проверить, что `add_pdf_api_key` применилась: `docker exec -i lms-sb-db-1 psql -U lms_user -d lms_db -c '\d "PdfApiKey"'`).
|
||||||
3. `docker pull ghcr.io/browserless/chromium` на Hoster.kz (интернет с сервера есть) + обновлённый compose.
|
3. `docker pull ghcr.io/browserless/chromium` на Hoster.kz (интернет с сервера есть) + обновлённый compose.
|
||||||
|
|||||||
Reference in New Issue
Block a user