6975a9f97e
- src/lib/email.ts: HTML templates for 4 email types (Second Brain design) - Welcome email on user registration (Better Auth databaseHooks) - Course access email when admin grants enrollment - Homework submitted email to all admins/curators (first submission only) - Feedback received email to student with feedback text and lesson link - Update TECHNICAL.md: Resend domain, from-address, email vars, Stage 3 summary Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
268 lines
13 KiB
Markdown
268 lines
13 KiB
Markdown
# TECHNICAL — LMS Second Brain
|
||
|
||
Живая документация проекта. Обновляется по мере разработки.
|
||
Роадмап и планирование — в `ROADMAP.md`. Здесь — факты о том, как всё устроено.
|
||
|
||
---
|
||
|
||
## Инфраструктура
|
||
|
||
| Компонент | Значение |
|
||
|---|---|
|
||
| **Сервер** | Hetzner VPS — 8 vCPU / 16 GB RAM / 320 GB NVMe |
|
||
| **IP** | 178.104.27.196 |
|
||
| **Домен LMS** | https://school.second-brain.ru |
|
||
| **Reverse proxy** | Caddy (auto HTTPS через Let's Encrypt) |
|
||
| **Порт приложения** | 3010 (внутри контейнера — 3000) |
|
||
| **БД** | PostgreSQL 16 (контейнер `lms-sb-db-1`) |
|
||
| **Object Storage** | Hetzner Object Storage, регион Nuremberg |
|
||
| **Бакет** | `second-brain-lms` (публичный, read-only) |
|
||
| **Endpoint S3** | https://nbg1.your-objectstorage.com |
|
||
| **Git-репозиторий** | https://git.second-brain.ru/admins/lms-sb |
|
||
| **Email-сервис** | Resend, домен `mailsend.second-brain.ru` (verified) |
|
||
| **From-адрес** | noreply@mailsend.second-brain.ru |
|
||
|
||
### Деплой
|
||
|
||
```bash
|
||
# На сервере: /root/digital-household/lms-sb/
|
||
git pull ...
|
||
docker compose -f docker-compose.prod.yml up -d --build
|
||
```
|
||
|
||
При старте контейнер автоматически запускает `prisma migrate deploy`, затем `node server.js`.
|
||
|
||
### .env на сервере
|
||
|
||
Файл `/root/digital-household/lms-sb/.env`:
|
||
|
||
```
|
||
DB_PASSWORD=lms_cd5041e961a3050db359aa15
|
||
BETTER_AUTH_SECRET=<secret>
|
||
RESEND_API_KEY=re_VX7TCnjs_N2geRqvveHjVfbsSyr4VbmzL
|
||
EMAIL_FROM=noreply@mailsend.second-brain.ru
|
||
S3_ENDPOINT=https://nbg1.your-objectstorage.com
|
||
S3_BUCKET=second-brain-lms
|
||
S3_ACCESS_KEY=<ключ>
|
||
S3_SECRET_KEY=<секрет>
|
||
S3_REGION=eu-central
|
||
```
|
||
|
||
---
|
||
|
||
## Стек
|
||
|
||
| Слой | Технология | Версия |
|
||
|---|---|---|
|
||
| Фреймворк | Next.js (App Router) | 16.2.2 |
|
||
| Язык | TypeScript | 5.x |
|
||
| UI | React | 19 |
|
||
| Стили | Tailwind CSS (CSS-based config) | 4.x |
|
||
| UI-компоненты | shadcn/ui (базируется на Base UI, **не Radix**) | latest |
|
||
| ORM | Prisma | 7.x |
|
||
| Auth | Better Auth | 1.6.0 |
|
||
| WYSIWYG | TipTap | 2.x |
|
||
| Drag-and-drop | @dnd-kit | latest |
|
||
| Шрифт | Fira Mono (400/500/700, Latin + Cyrillic) | Google Fonts |
|
||
| Email | Resend | latest |
|
||
| S3 | @aws-sdk/client-s3 | 3.x |
|
||
| БД | PostgreSQL | 16 |
|
||
|
||
### Важные нюансы стека
|
||
|
||
- **shadcn/ui v4** использует `@base-ui/react`, а не Radix. Нет `asChild`. Триггеры — обычные элементы.
|
||
- **Prisma 7** не генерирует `index.ts`. Импорт: `from "@/generated/prisma/client"`, не `from "@/generated/prisma"`.
|
||
- **Prisma 7** требует адаптер: `new PrismaPg({ connectionString })` — иначе `PrismaClient()` бросает ошибку.
|
||
- **Better Auth** использует `scrypt` по умолчанию. В этом проекте **переключён на bcrypt** (в `auth.ts` настроены `password.hash` / `password.verify`).
|
||
- **`NEXT_PUBLIC_*`** переменные запекаются при сборке. `auth-client.ts` не использует `baseURL` — клиент сам берёт `window.location.origin`.
|
||
- **Next.js 16** использует `proxy.ts` вместо `middleware.ts` (и экспортируемая функция называется `proxy`, не `middleware`).
|
||
- **Tailwind v4**: конфиг только в CSS через `@import "tailwindcss"` и `@theme`. Нет `tailwind.config.ts`.
|
||
|
||
---
|
||
|
||
## Дизайн-система
|
||
|
||
Стиль: **Second Brain Aubade** — типографский, монохромный, с газетным характером.
|
||
|
||
| Токен | Значение |
|
||
|---|---|
|
||
| Шрифт | Fira Mono (весь UI) |
|
||
| Фон страницы | `#F5F5F0` (тёплый off-white) |
|
||
| Текст основной | `#323232` (тёмный уголь) |
|
||
| Текст вторичный | `#666666` |
|
||
| Поверхность / surface | `#E8E8E0` |
|
||
| Акцент / highlight | `#E8F0D8` (зелёный) |
|
||
| Divider / border | `#AAAAAA` |
|
||
| Hover | `#D8D8D0` |
|
||
| Фон сайдбара (тёмный) | `#2A2A28` |
|
||
| Активный пункт сайдбара | `#E8F0D8` (зелёный) |
|
||
|
||
**Aubade-эффект** — фирменный стиль карточек и кнопок:
|
||
- Border: `2px solid #AAAAAA`
|
||
- Box-shadow: `4px 4px 0 0 #AAAAAA` (смещение без размытия)
|
||
- Hover: `transform: translate(-2px, -2px)` + shadow `6px 6px`
|
||
- Active (кнопка): `transform: translate(2px, 2px)` + shadow убирается
|
||
|
||
CSS-классы: `.card-aubade`, `.btn-aubade`, `.btn-aubade-accent`, `.tag-aubade`
|
||
|
||
---
|
||
|
||
## Требования к медиафайлам
|
||
|
||
### Обложка курса (`Course.coverImage`)
|
||
|
||
| Параметр | Требование |
|
||
|---|---|
|
||
| **Соотношение сторон** | **16 : 9** (горизонтальный прямоугольник) |
|
||
| **Рекомендуемое разрешение** | 1280 × 720 px (HD) или 1920 × 1080 px (Full HD) |
|
||
| **Минимальное разрешение** | 800 × 450 px |
|
||
| **Максимальный размер файла** | 5 MB |
|
||
| **Форматы** | JPG, PNG, WebP |
|
||
| **Цветовое пространство** | sRGB |
|
||
| **Где хранится** | Hetzner Object Storage, бакет `second-brain-lms`, путь `uploads/<uuid>.ext` |
|
||
| **Доступ** | Публичный URL (прямая ссылка на файл) |
|
||
|
||
> Пример URL: `https://nbg1.your-objectstorage.com/second-brain-lms/uploads/abc123.jpg`
|
||
|
||
### Изображения в уроках (TipTap)
|
||
|
||
| Параметр | Требование |
|
||
|---|---|
|
||
| **Соотношение сторон** | Любое — TipTap встраивает как `<img>` с `max-width: 100%` |
|
||
| **Рекомендуемая ширина** | 1200 px (контент-зона урока) |
|
||
| **Максимальный размер файла** | 10 MB |
|
||
| **Форматы** | JPG, PNG, GIF, WebP |
|
||
| **Где хранится** | Hetzner Object Storage, путь `uploads/<uuid>.ext` |
|
||
|
||
### PDF и файлы к уроку (Этап 2+)
|
||
|
||
| Параметр | Требование |
|
||
|---|---|
|
||
| **Форматы** | PDF, ZIP, DOCX, XLSX, PPTX |
|
||
| **Максимальный размер** | 100 MB |
|
||
| **Где хранится** | Hetzner Object Storage, путь `lessons/<lessonId>/files/<uuid>.ext` |
|
||
|
||
### Аватары пользователей (если добавим)
|
||
|
||
| Параметр | Требование |
|
||
|---|---|
|
||
| **Соотношение сторон** | 1 : 1 (квадрат) |
|
||
| **Рекомендуемый размер** | 256 × 256 px |
|
||
| **Максимальный размер файла** | 2 MB |
|
||
| **Форматы** | JPG, PNG, WebP |
|
||
|
||
---
|
||
|
||
## Роли и доступ
|
||
|
||
| Роль | Маршруты | Описание |
|
||
|---|---|---|
|
||
| `admin` | `/admin/*`, `/curator/*`, `/dashboard` | Полный доступ |
|
||
| `curator` | `/curator/*`, `/dashboard` | Проверка ДЗ, комментарии |
|
||
| `student` | `/dashboard`, `/courses/*` | Просмотр курсов, прогресс |
|
||
|
||
Защита маршрутов — в `src/proxy.ts` + проверка сессии в каждом layout/page.
|
||
|
||
---
|
||
|
||
## API-маршруты
|
||
|
||
| Метод | Путь | Описание | Кто может |
|
||
|---|---|---|---|
|
||
| `POST` | `/api/auth/[...all]` | Better Auth handler | Все |
|
||
| `POST` | `/api/admin/upload` | Загрузка файла в S3, возвращает `{ url, key }` | admin |
|
||
|
||
---
|
||
|
||
## Структура БД (ключевые таблицы)
|
||
|
||
```
|
||
User — id, email, name, role, emailVerified
|
||
Session — Better Auth sessions
|
||
Account — Better Auth credentials (bcrypt password)
|
||
Verification — Better Auth email verification tokens
|
||
|
||
Category — id, title, slug, order
|
||
Course — id, slug, title, description, coverImage, published, order, categoryId
|
||
Module — id, courseId, title, order
|
||
Lesson — id, moduleId, title, content (JSON), kinescopeId, published, order
|
||
LessonFile — id, lessonId, name, url, size
|
||
|
||
CourseEnrollment — userId + courseId (PK), enrolledAt, expiresAt
|
||
AccessLog — id, courseId, userId, action, method, grantedById, note, createdAt
|
||
LessonProgress — userId + lessonId (PK), completedAt
|
||
|
||
Quiz — id, lessonId, showAnswers
|
||
QuizQuestion — id, quizId, text, type (SINGLE/MULTIPLE/TEXT), order
|
||
QuizOption — id, questionId, text, isCorrect, order
|
||
QuizAttempt — id, userId, quizId, score, answers (JSON), completedAt
|
||
|
||
Homework — id, lessonId, description
|
||
HomeworkSubmission — id, homeworkId, userId, text, files (JSON), submittedAt
|
||
HomeworkFeedback — id, submissionId, curatorId, text, createdAt
|
||
|
||
LessonComment — id, lessonId, userId, text, deleted, createdAt
|
||
```
|
||
|
||
Миграции: `prisma/migrations/` — **никогда не редактировать вручную**.
|
||
|
||
---
|
||
|
||
## Тестовые аккаунты (seed)
|
||
|
||
| Email | Пароль | Роль |
|
||
|---|---|---|
|
||
| admin@second-brain.ru | Password123! | admin |
|
||
| curator@second-brain.ru | Password123! | curator |
|
||
| student@second-brain.ru | Password123! | student |
|
||
|
||
---
|
||
|
||
## Что сделано (по этапам)
|
||
|
||
### Этап 0 — Каркас + Auth ✅
|
||
- Next.js 16.2.2 + TypeScript + Tailwind v4
|
||
- PostgreSQL 16 + Prisma 7 + полная LMS-схема
|
||
- Better Auth: email/password, роли, сессии
|
||
- proxy.ts: защита маршрутов
|
||
- Дашборды для 3 ролей (admin / curator / student)
|
||
- Dockerfile multi-stage + docker-compose.prod.yml
|
||
- Caddy: school.second-brain.ru → порт 3010
|
||
|
||
### Этап 1 — CRUD курсов в админке ✅
|
||
- Список курсов: `/admin/courses`
|
||
- Создание курса (диалог), редактирование, удаление
|
||
- Обложка курса: загрузка в S3, требования — см. раздел «Медиафайлы»
|
||
- Модули: drag-and-drop сортировка, CRUD
|
||
- Уроки: drag-and-drop сортировка, CRUD
|
||
- Редактор урока: TipTap (Bold, Italic, H2/H3, списки, цитата, код, ссылки, изображения)
|
||
- Загрузка изображений в урок → S3
|
||
- Поле Kinescope ID (текстовое)
|
||
- Публикация / скрытие курса и урока
|
||
- Управление доступом к курсу (выдать / отозвать)
|
||
- Страница пользователей: `/admin/users`
|
||
- Дизайн Second Brain Aubade (Fira Mono, #F5F5F0, карточки с тенью)
|
||
|
||
### Этап 1.5 — Расширенное управление доступом ✅
|
||
- Категории курсов: `/admin/categories`, CRUD, привязка к курсу
|
||
- Срок доступа: поле `expiresAt` при энролле, просроченный подсвечивается красным
|
||
- Страница ученика `/admin/users/[userId]`: мультиэнролл (несколько курсов + срок)
|
||
- История доступа: таблица `AccessLog`, отображается на странице курса и ученика
|
||
- Hetzner Object Storage подключён: бакет `second-brain-lms`, Nuremberg
|
||
|
||
### Этап 3 — Прогресс, ДЗ, Email ✅
|
||
- Прогресс студента: кнопка «Отметить как пройденный», галочки и прогресс-бар в сайдбаре, прогресс на дашборде
|
||
- Домашние задания: редактор ДЗ в уроке (admin), сдача текстом + файлами (student), проверка и фидбек (curator/admin)
|
||
- Куратор-панель: `/curator/dashboard` со статистикой, `/curator/homework` список, страница проверки
|
||
- Админ имеет доступ к куратор-маршрутам через пункт «ДЗ на проверку» в сайдбаре
|
||
- Email уведомления через Resend: доступ к курсу, новая работа, фидбек получен, приветствие
|
||
|
||
---
|
||
|
||
## Известные ограничения / технический долг
|
||
|
||
- `requireEmailVerification: true` в Better Auth — seed-пользователи вставлены напрямую через SQL с `emailVerified = true`
|
||
- Загрузка файлов через `/api/admin/upload` — нет ограничения по размеру на уровне Next.js (только S3). При необходимости добавить middleware с проверкой `Content-Length`
|
||
- Drag-and-drop обновляет порядок через Server Actions — при быстрых последовательных перетаскиваниях возможны race conditions (некритично для MVP)
|
||
- `expiresAt` проверяется только в UI (красная подсветка). Блокировка доступа по сроку на уровне middleware — в рамках Этапа 2
|