Files
lms-sb/TECH_DEBT_AUDIT.md
admins 444b9c0faf Apply tech debt fixes: middleware rename, React.cache, file size limits, remove dead deps
- Rename proxy.ts → middleware.ts, export proxy() → middleware() so Next.js edge
  protection actually activates (F001)
- Add PUBLIC_ROUTES entries for /forgot-password and /reset-password
- Wrap getSettings() in React.cache() to eliminate duplicate DB call in root layout (F003)
- Remove 4 console.log calls from saveLesson Server Action, keep console.error (F005)
- Add 50 MB file size guard to all 6 upload routes before arrayBuffer() read (F004)
- Remove unused deps: @tailwindcss/typography, shadcn, tw-animate-css (F008)
- Update CLAUDE.md: Prisma version 6.x → 7.x
- Add TECH_DEBT_AUDIT.md with 14 findings across 9 dimensions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 19:35:05 +05:00

15 KiB
Raw Permalink Blame History

Tech Debt Audit — lms-sb

Generated: 2026-05-09


Executive Summary

  • 1 Critical: middleware не работает — файл называется proxy.ts вместо middleware.ts, защита маршрутов на уровне Next.js отсутствует
  • 3 High: двойная загрузка полной структуры курса на каждый урок; getSettings() вызывается дважды в root layout; нет ограничения размера загружаемых файлов
  • 0 тестов — ни одного test-файла во всём проекте
  • 4 отладочных console.log в production-коде Server Action
  • Zod установлен как зависимость, но нигде не используется — Server Actions принимают FormData без валидации схемы
  • 3 уязвимости npm high severity (next, fast-uri, fast-xml-builder)
  • settings-form.tsx — 506 строк, единственный god-файл, но внутренняя структура оправданна
  • Самые горячие файлы совпадают с самыми крупными: student lesson page (12 правок, 270 строк) и lesson-editor (8 правок, 408 строк) — концентрация долга

Architectural Mental Model

LMS построена на Next.js 16 App Router с тремя зонами доступа: (auth), (student), admin/curator. Мутации идут через Server Actions, данные читаются в RSC. Better Auth отвечает за сессии и роли. Prisma 7 с PostgreSQL через собственный PrismaPg адаптер (обходит ограничения Turbopack).

Главная аномалия: middleware объявлен в src/proxy.ts с функцией proxy(), а не в src/middleware.ts с функцией middleware(). Next.js его не подхватывает. Защита работает только за счёт явных проверок сессии в каждой странице и action — что само по себе достаточно, но заявленный в CLAUDE.md "Auth middleware (защита маршрутов)" фактически не существует.

Второй структурный факт: при открытии страницы урока студентом происходит двойная загрузка полной структуры курса — один раз в layout.tsx (для sidebar), второй в page.tsx (для prev/next навигации). Это N+1 на уровне layout/page.


Findings

ID Category File:Line Severity Effort Description Recommendation
F001 Security src/proxy.ts:1 Critical S Файл proxy.ts не является Next.js middleware. Next.js ищет src/middleware.ts с экспортом middleware. Маршруты не защищены на уровне edge. Переименовать файл в src/middleware.ts, переименовать экспорт proxymiddleware
F002 Performance src/app/(student)/courses/[slug]/lessons/[lessonId]/page.tsx:37 High M page.tsx загружает lesson.module.course.modules с вложенными уроками для prev/next nav — та же структура уже загружена в layout.tsx:20. Двойной DB-запрос на каждый pageview урока. Вынести prev/next навигацию в layout или передавать через searchParams/context вместо повторной загрузки
F003 Performance src/app/layout.tsx:14,27 High S getSettings() вызывается дважды в одном компоненте — в generateMetadata() и в RootLayout(). Два одинаковых DB-запроса на каждый запрос. Объединить в один вызов или обернуть getSettings в React.cache()
F004 Security src/app/api/admin/upload/route.ts:1, src/app/api/student/homework-upload/route.ts:1, src/app/api/curator/audio-upload/route.ts:1 High S Ни один upload endpoint не проверяет размер файла перед file.arrayBuffer(). Загрузка 1 ГБ файла ляжет в память Node.js. Добавить проверку file.size до 50 МБ (или другого лимита) сразу после form.get("file")
F005 Observability src/lib/actions/lesson-actions.ts:24,27,42,48 Medium S 4 console.log в production Server Action. Логируют lessonId и статус каждого сохранения в prod-консоль. Убрать все 4. Оставить только console.error в catch-блоках.
F006 Type & Contract src/app/admin/courses/actions.ts:28-47 Medium M Server Actions принимают FormData и читают поля через as string без валидации. Zod установлен, но не используется нигде в проекте. Добавить Zod-схему на входе createCourse и updateCourse; повторить паттерн в остальных actions
F007 Dependencies package.json Medium S npm audit показывает 3 high severity уязвимости: next (сам фреймворк), fast-uri, fast-xml-builder. npm update next до последнего патча; проверить влияние на остальные зависимости
F008 Dependencies package.json Low S depcheck находит 4 неиспользуемые зависимости: @tailwindcss/typography, shadcn, tw-animate-css, zod. Если Zod будет использован (F006), убрать оставшиеся три. npm remove @tailwindcss/typography shadcn tw-animate-css
F009 Architecture src/app/(student)/courses/[slug]/lessons/[lessonId]/page.tsx:102-106 Low S Функция formatSize определена внутри Server Component — при каждом рендере пересоздаётся. Не ошибка, но засоряет файл. Вынести в src/lib/utils.ts
F010 Consistency src/app/admin/courses/[courseId]/actions.ts:12, src/app/admin/categories/actions.ts:11, src/app/admin/settings/actions.ts:11 Low S Разные строки ошибки авторизации: "Forbidden" (EN), "Нет доступа" (RU), "Unauthorized" (EN). Нет единого паттерна. Выбрать один формат и применить везде; ошибки авторизации не должны уходить клиенту как читаемый текст
F011 Security src/app/layout.tsx:32,37 Low dangerouslySetInnerHTML с headCode/bodyCode из БД — admin может вставить произвольный JS. Намеренная функция (code injection для аналитики). Задокументировать явно, что это admin-only привилегия. Добавить проверку роли на странице настроек — уже есть.
F012 Testing High L Ни одного теста во всём проекте. Нет *.test.*, *.spec.*, нет __tests__/. Горячие файлы (lesson page, lesson-editor) не прикрыты ничем. Начать с unit-тестов src/lib/md-to-tiptap.ts (чистая функция, высокий риск регрессии) и src/lib/settings.ts. Для UI — Playwright E2E на login + lesson complete flow.
F013 Consistency src/app/(student)/courses/[slug]/layout.tsx:54, src/app/(student)/courses/[slug]/lessons/[lessonId]/page.tsx:53 Low S Оба файла самостоятельно проверяют isAdmin для conditional DB queries с одинаковой логикой. Паттерн не вынесен. Не критично при текущем размере, но при росте числа маршрутов станет проблемой
F014 Documentation src/proxy.ts:1, CLAUDE.md Medium S CLAUDE.md: src/middleware.ts — Auth middleware (защита маршрутов). Файла не существует, существует src/proxy.ts. Документация врёт. После исправления F001 — обновить CLAUDE.md

Top 5 "fix these first"

1. F001 — Переименовать proxy.ts в middleware.ts

git mv src/proxy.ts src/middleware.ts

В src/middleware.ts:

// было:
export function proxy(request: NextRequest) { ... }
// стало:
export function middleware(request: NextRequest) { ... }

config экспорт уже правильный — оставить как есть. Это однострочный фикс с нулевым риском регрессии.


2. F003 — Двойной getSettings() в root layout

// src/app/layout.tsx — было: два вызова
const settings = await getSettings(); // в generateMetadata
const settings = await getSettings(); // в RootLayout

// стало: обернуть в React.cache
// src/lib/settings.ts
import { cache } from "react";
export const getSettings = cache(async (): Promise<Settings> => { ... });

Один cache() снимает оба дублированных запроса в рамках одного render pass.


3. F004 — Лимит размера файлов в upload endpoints

В каждом из 5 upload routes добавить сразу после получения файла:

const MAX_BYTES = 50 * 1024 * 1024; // 50 МБ
if (file.size > MAX_BYTES) {
  return NextResponse.json({ error: "Файл слишком большой" }, { status: 413 });
}

4. F005 — Убрать console.log из lesson-actions.ts

// src/lib/actions/lesson-actions.ts — удалить строки 24, 27, 42, 48
console.log("[saveLesson] start", lessonId);   // удалить
console.log("[saveLesson] auth ok");           // удалить
console.log("[saveLesson] db update ok");      // удалить
console.log("[saveLesson] done");              // удалить

5. F002 — Устранить двойную загрузку структуры курса

layout.tsx уже загружает все модули/уроки курса для sidebar. page.tsx загружает ту же структуру ещё раз для prev/next навигации. Самое чистое решение — передавать allLessons через searchParams или вычислять в layout и передавать через slot:

Альтернатива проще: убрать из page.tsx include: { modules: { include: { lessons } } } и принять prevLessonId/nextLessonId как query params, которые layout прописывает в ссылки sidebar-а.


Quick Wins

  • F001git mv src/proxy.ts src/middleware.ts + переименовать экспорт (5 минут)
  • F003import { cache } from "react" в settings.ts, обернуть getSettings (10 минут)
  • F005 — Удалить 4 строки console.log в lesson-actions.ts (2 минуты)
  • F007npm update next — закрыть CVE в самом фреймворке
  • F008npm remove @tailwindcss/typography shadcn tw-animate-css — убрать мёртвый вес
  • F004 — Добавить file.size проверку в 5 upload routes (15 минут)

Things that look bad but are actually fine

src/generated/prisma/ — 20+ сгенерированных файлов в src/. Выглядит как мусор, но это намеренно: Prisma 7 с Turbopack требует TypeScript-клиента в src для корректной работы RSC. Объяснено в коммите af8644e и в memory-файле project_lms_prisma_config.md. Не трогать.

src/app/(student)/courses/[slug]/layout.tsx:54prisma.lessonProgress.findMany({ where: { lessonId: { in: allLessonIds } } }) загружает прогресс по всем урокам курса. Выглядит избыточно, но это единственный способ отрисовать sidebar с чекбоксами без N+1 запроса на каждый урок. Правильный паттерн.

src/lib/auth.ts:37 — Хардкод "https://school.second-brain.ru" в trustedOrigins. Выглядит как нарушение правила "нет захардкоженных URL". На самом деле это security-критичный список и он должен быть явным, не конфигурируемым через переменные (иначе можно было бы переопределить в .env). Оставить.

catch (() => {}) в трёх местах (email в import, S3 delete) — выглядит как проглатывание ошибок. В контексте это правильно: ошибка отправки welcome-email или удаления старого файла из S3 не должна ронять основную операцию (импорт/апгрейд файла).

506 строк в settings-form.tsx — формально god-файл, но это один большой конфиг-экран с однородной структурой (Section + Field + Toggle). Файл читается линейно, нет запутанной логики. Декомпозиция не добавит ясности.


Open Questions

  1. forgot-password и reset-password маршруты не входят в PUBLIC_ROUTES в proxy.ts:4. Это намеренно — эти страницы требуют cookie для валидации токена? Или просто забыто?

  2. src/app/api/admin/import-md/route.ts — существует, но нет UI для вызова. Мёртвый endpoint или WIP?

  3. QuizOption — схема Prisma содержит QuizOption с isCorrect, но в page.tsx урока quiz загружается без options (include: { questions: { orderBy: { order: "asc" } } }). Тест работает только с open-ended вопросами или options загружаются где-то ещё?

  4. load-test.js в корне репо — k6 отмечен как missing dependency в depcheck. Это намеренно отдельный инструмент или планируется CI-интеграция?