12 KiB
12 KiB
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 |
Деплой
# На сервере: /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=<пусто — заполнить при настройке email>
EMAIL_FROM=noreply@school.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 |
| 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)+ shadow6px 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)
| Пароль | Роль | |
|---|---|---|
| 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
Известные ограничения / технический долг
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