Files
lms-sb/TECHNICAL.md
T

12 KiB
Raw Blame History

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
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 по умолчанию. В этом проекте переключён на bcryptauth.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

Известные ограничения / технический долг

  • 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