- 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>
15 KiB
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, переименовать экспорт proxy → middleware |
| 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
- F001 —
git mv src/proxy.ts src/middleware.ts+ переименовать экспорт (5 минут) - F003 —
import { cache } from "react"в settings.ts, обернутьgetSettings(10 минут) - F005 — Удалить 4 строки
console.logв lesson-actions.ts (2 минуты) - F007 —
npm update next— закрыть CVE в самом фреймворке - F008 —
npm 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:54 — prisma.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
-
forgot-passwordиreset-passwordмаршруты не входят вPUBLIC_ROUTESвproxy.ts:4. Это намеренно — эти страницы требуют cookie для валидации токена? Или просто забыто? -
src/app/api/admin/import-md/route.ts— существует, но нет UI для вызова. Мёртвый endpoint или WIP? -
QuizOption — схема Prisma содержит
QuizOptionсisCorrect, но вpage.tsxурока quiz загружается безoptions(include: { questions: { orderBy: { order: "asc" } } }). Тест работает только с open-ended вопросами или options загружаются где-то ещё? -
load-test.jsв корне репо —k6отмечен как missing dependency в depcheck. Это намеренно отдельный инструмент или планируется CI-интеграция?