Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e2311facf4 | ||
|
|
30d3523aeb | ||
|
|
02443f0c5c |
@@ -197,6 +197,14 @@ S3_REGION="eu-central"
|
|||||||
- **`src/proxy.ts` не существует** — вся защита маршрутов в `src/middleware.ts`, экспорт `middleware`. Не заводить второй файл.
|
- **`src/proxy.ts` не существует** — вся защита маршрутов в `src/middleware.ts`, экспорт `middleware`. Не заводить второй файл.
|
||||||
- Новый API-роут, который авторизуется сам (Bearer-ключ, внутренний секрет), обязан попасть в `PUBLIC_ROUTES` — иначе middleware отдаст редирект на `/login` раньше роута, и в браузере с кукой это не воспроизведётся. Обратная сторона: сверка идёт по `startsWith`, поэтому запись `/api/pdf` уже открывает любой будущий `/api/pdf-*` — такой роут обязан проверять доступ внутри себя.
|
- Новый API-роут, который авторизуется сам (Bearer-ключ, внутренний секрет), обязан попасть в `PUBLIC_ROUTES` — иначе middleware отдаст редирект на `/login` раньше роута, и в браузере с кукой это не воспроизведётся. Обратная сторона: сверка идёт по `startsWith`, поэтому запись `/api/pdf` уже открывает любой будущий `/api/pdf-*` — такой роут обязан проверять доступ внутри себя.
|
||||||
|
|
||||||
|
### Уведомления и письма
|
||||||
|
|
||||||
|
- **Отправка глотает ошибки.** Каждый `send*Email` заканчивается `.catch((e) => console.error(...))`, поэтому «серверное действие выполнилось» ничего не говорит о письме. Проверять боем только по Resend: `GET https://api.resend.com/emails?limit=…` и поле `last_event` (`delivered` / `bounced`). Ключ — `~/.config/secrets/resend.env`.
|
||||||
|
- **Уведомления висят на серверном действии, а не на таблице.** Строка, вставленная в базу напрямую (`INSERT INTO "LessonComment" …`), писем не шлёт — это удобно, чтобы подготовить сцену, но проверять нужно через интерфейс. При автоматизации браузером легко промахнуться мимо формы ответа и попасть в корневое поле: тогда `parentId` пустой, код-путь не вызывается и письма правомерно нет. Помечать поля до клика «Ответить» и брать `textarea:not([data-pre])`.
|
||||||
|
- **Перед правкой серверного действия проверить, кто его импортирует.** В проекте жили две расходящиеся копии `addComment`/`deleteComment`/`editComment`: живая в `src/lib/actions/student-actions.ts` (лимит 10 000) и мёртвая в `src/app/(student)/courses/[slug]/lessons/[lessonId]/comment-actions.ts` (лимит 2000, никем не импортировалась, удалена 20260915). Правка в неподключённой копии выглядит применённой и не даёт эффекта.
|
||||||
|
- **Дефолты настроек живут в коде.** В прод-таблице `Settings` строк `notify*` нет вообще — значения берутся из `SETTINGS_DEFAULTS` (`src/lib/settings.ts`), строка появляется только при первом сохранении формы в админке. Новый ключ с дефолтом `"true"` включается на проде сам, без миграции и без действий в интерфейсе.
|
||||||
|
- Лимиты превью в письмах разные по смыслу: `REPLY_PREVIEW_LIMIT = 2000` у тредов вопросов, `COMMENT_REPLY_PREVIEW_LIMIT = 4000` у ответов под уроками (ввод разрешает 10 000, разборы под уроком длиннее реплик в треде). Не сводить в одну константу.
|
||||||
|
|
||||||
### Чистый PDF (`/tools/clean-pdf`, `/api/pdf`)
|
### Чистый PDF (`/tools/clean-pdf`, `/api/pdf`)
|
||||||
|
|
||||||
- **Два разных значения `ToolUsage.tool`**: `clean-pdf-generate` (`PDF_TOOL_ID`) — только успешные генерации, по ним считаются месячный лимит и burst; `clean-pdf` — аналитика копирований из `CopyButton`. Не объединять: иначе каждое нажатие «Скопировать» списывает генерацию. `clean-pdf-generate` намеренно отсутствует в `TOOL_IDS`, чтобы публичный Server Action не мог списать квоту.
|
- **Два разных значения `ToolUsage.tool`**: `clean-pdf-generate` (`PDF_TOOL_ID`) — только успешные генерации, по ним считаются месячный лимит и burst; `clean-pdf` — аналитика копирований из `CopyButton`. Не объединять: иначе каждое нажатие «Скопировать» списывает генерацию. `clean-pdf-generate` намеренно отсутствует в `TOOL_IDS`, чтобы публичный Server Action не мог списать квоту.
|
||||||
@@ -232,6 +240,11 @@ S3_REGION="eu-central"
|
|||||||
|
|
||||||
`**жирный**` в отзыве на ДЗ студент увидит звёздочками. Ссылку там же писать голым адресом можно — `linkify` её поднимет.
|
`**жирный**` в отзыве на ДЗ студент увидит звёздочками. Ссылку там же писать голым адресом можно — `linkify` её поднимет.
|
||||||
|
|
||||||
|
Когда сами пишем ответ студенту под уроком, две граблю рендера (проверено боем 20260922, урок Zotero 2.3):
|
||||||
|
|
||||||
|
- **Перечисление строками через «—» склеивается в один абзац.** GFM не считает тире маркером списка: четыре пункта подряд, каждый с новой строки, отрендерились одной сплошной строкой в ответе от 21.09. Список — только `- ` в начале строки.
|
||||||
|
- **Длинный адрес с `&` оборачивать в `<…>`.** Автолинк GFM на параметрах запроса обрывается; `<https://host/?a=1&b=2>` даёт кликабельную ссылку целиком. Подчёркивания внутри слова (`url_selector`) курсивом не становятся — их экранировать не нужно.
|
||||||
|
|
||||||
Безопасность рендера комментариев (не ослаблять «улучшением»): `rehype-sanitize` режет сырой HTML и `javascript:`; картинки с чужих доменов **не грузятся** — компонент `img` отдаёт их ссылкой, чтобы адрес читателя не утекал на сторонний сервер. Свои картинки — через `/api/student/comment-upload`.
|
Безопасность рендера комментариев (не ослаблять «улучшением»): `rehype-sanitize` режет сырой HTML и `javascript:`; картинки с чужих доменов **не грузятся** — компонент `img` отдаёт их ссылкой, чтобы адрес читателя не утекал на сторонний сервер. Свои картинки — через `/api/student/comment-upload`.
|
||||||
|
|
||||||
### Файлы курсов живут на двух разных доменах
|
### Файлы курсов живут на двух разных доменах
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export const PAY_METHODS_URL = "https://obsidian.second-brain.ru/pay/methods";
|
|||||||
export const STORE_CATALOG: Record<string, StoreCatalogEntry> = {
|
export const STORE_CATALOG: Record<string, StoreCatalogEntry> = {
|
||||||
"second-brain-vault": {
|
"second-brain-vault": {
|
||||||
landingUrl: "https://second-brain.ru/sb-vault",
|
landingUrl: "https://second-brain.ru/sb-vault",
|
||||||
tariffs: [{ sku: "sb-vault", label: "Second Brain Vault", priceRub: 2990 }],
|
tariffs: [{ sku: "sb-vault", label: "Second Brain Vault", priceRub: 4990 }],
|
||||||
},
|
},
|
||||||
"claude-obsidian": {
|
"claude-obsidian": {
|
||||||
landingUrl: "https://second-brain.ru/claude-obsidian",
|
landingUrl: "https://second-brain.ru/claude-obsidian",
|
||||||
|
|||||||
Reference in New Issue
Block a user