Notify admins/curators (or configured notificationEmails) when a student
leaves a comment under a lesson. New setting notifyOnComment (default on) +
toggle in admin settings. Fires only for student comments, not admin/curator
replies; comment text is HTML-escaped in the email.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two more inside-LMS generators under /tools, completing the 6-tool set:
- Dataview: DQL builder (TABLE/LIST/TASK/CALENDAR, FROM/WHERE/SORT/LIMIT),
output wrapped in a dataview code block
- Bases: .base YAML builder (views, filters and:, order, sort, groupBy, limit);
filter expressions emitted as single-quoted YAML scalars for safety
Syntax verified against official Obsidian docs (research workflow). Audit fix:
Bases sort entries use 'column:' (not 'property:') per documented working .base
files; 'property:' is kept only for groupBy. 14 new tests (47 total).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Inside-LMS toolbox for registered students: callout-CSS, YAML-frontmatter,
theme CSS-variables, Style Settings generators. Auth inherited from (student)
route group; no public surface, no email gate.
- Pure generators in src/lib/tools/* with Vitest (33 tests)
- Shared YAML-safe scalar quoter (_shared/yaml.ts) used by frontmatter +
style-settings: unquoted user input was silently breaking YAML (tags '#x'
-> null, color default '#7C3AED' -> null, 'a: b' -> parse error)
- hex validation in callout (no NaN), date input constrained, clipboard +
analytics calls guarded
- Prisma ToolUsage model + migration; logToolUsage Server Action (auth-first,
10-min dedup window per user/tool)
- Tool index /tools + ToolCard (explicit lucide map, no import *) + header link
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Attachments uploaded via question-upload get a CDN URL
(files.second-brain.ru via S3_CDN_URL), but the validator only accepted
the direct S3 endpoint prefix — so every attachment was silently dropped
(message text saved, file lost). Add isAllowedPublicUrl (CDN + direct S3)
in lib/s3 and use it in both question routes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Curators/admins can mark a submission APPROVED (and complete the lesson)
without writing feedback. The student sees 'ДЗ принято ✓' and can no
longer resubmit. No email is sent on silent approval.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
getPublicUrl returns the CDN URL (files.second-brain.ru via Bunny) when
S3_CDN_URL is configured, falling back to the direct S3 endpoint. This
was a manual patch on the build server that was never committed —
restore it so new uploads also get CDN-backed (RU-accessible) URLs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Lesson files were served inline (no Content-Disposition) → browsers
opened PDFs in a new tab instead of downloading; flaky across
browsers/providers. Set Content-Disposition: attachment with the
human-readable filename (RFC 5987 for Cyrillic) on upload.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- /api/internal/grant: secret-auth endpoint for payment-router to
provision access on a paid order — find-or-create user (emailVerified,
temp password, mustChangePassword), enroll by course slug list,
AccessLog, delivery email. Idempotent by order_id.
- User.mustChangePassword flag (+ migration); (student) layout redirects
flagged users to /change-password (forced first-login change).
- email.ts: sendCourseGrantEmail (temp password for new buyers).
- middleware: /api/internal is public (own secret auth).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Block direct /api/auth/sign-up at the middleware (404): it bypassed
the Turnstile/honeypot wrapper. /api/register is unaffected — it
invokes auth.handler programmatically, not through HTTP.
- Tighten rate limits on send-verification-email and forget-password
(3/min/IP): both send emails to arbitrary addresses and shared the
loose global 100/min limit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
requireEmailVerification was enabled but no sendVerificationEmail was
configured — new users saw "check your email", never received anything,
and couldn't log in (403 shown as "wrong email or password").
- email.ts: add verification email template; welcome email no longer
claims the account is "confirmed"
- auth.ts: configure emailVerification (send on signup, resend on
unverified login attempt, auto sign-in after verification, 24h TTL)
- register route: callbackURL=/dashboard so the verify link lands in
the cabinet
- login form: distinguish 403 (unverified — tell user a fresh link was
sent) and 429 (rate limit) from wrong credentials
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Add CLOSED status guard in messages POST (returns 409)
- Add extension allowlist check in upload route + text/x-markdown MIME type
- Sanitize files JSON array before DB write
- Add sendQuestionFollowUpEmail helper and use it for student follow-up replies
- Scope email field to staff only in questions list query
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Users can now request a password reset link via email. Better Auth
sendResetPassword callback sends a branded email via Resend. Login
page shows success notice after password is set.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add coverImage field to Lesson model (prisma)
- Pass coverImage as poster prop to KinescopePlayer
- Show quiz in read-only preview mode for admin on lesson page
- Fix TipTap v3 editor reset on save: pass [lesson.id] as deps to useEditor
to prevent setOptions() from reinitializing content on every re-render
- Replace saveLesson Server Action call with fetch PATCH /api/admin/lessons/[id]
to avoid Next.js 16 automatic RSC refresh after Server Actions
- Simplify revalidatePath: only revalidate module page, not lesson editor page
- Add parentId field to LessonComment (self-referential FK, SetNull on delete)
- Show replies indented under parent comment with left border visual
- Add reply button (visible to admin/curator only) with inline textarea
- Only root comments shown in main list; replies nested below their parent
- Update comment count to include replies
- Server-side validation: only admin/curator can reply, parent must belong to same lesson
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Wire settings to actual platform behavior: maintenance mode, registration toggle,
notification emails, curator feedback emails, email verification flag
- Add logo (logoUrl, showLogo) and social network links (YouTube, VK, Telegram) settings
- Show logo + school name dynamically in student layout header
- Add footer to student layout with org requisites and social links
- Register page: read settings server-side, validate terms checkbox with legal links
- Login page: show notice when redirected from closed registration
- Settings form: add Logo and Social Networks sections
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
React treats TipTap's editor.getJSON() output as a non-plain object when
passed through Server Action serialization, causing isDecimal() inside
Prisma's query builder to receive a temporary client reference proxy and
throw ERROR 1213974697. JSON.parse(JSON.stringify()) strips the prototype
chain and any non-enumerable properties, ensuring Prisma receives a
clean plain object.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Use @/generated/prisma/client instead of @prisma/client to avoid
Turbopack creating a broken external proxy for the missing
.prisma/client/default module at runtime. Add @prisma/adapter-pg
to serverExternalPackages, remove unused resolveAlias.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The new prisma-client generator outputs TypeScript files to src/generated/prisma/
which include import.meta.url at module level. Turbopack sees this and marks the
entire module as a client reference, causing 'Cannot access toStringTag on the
server' on every page that uses Prisma.
Switching to prisma-client-js puts the generated client in node_modules/@prisma/client
where serverExternalPackages can properly exclude it from the server bundle.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All admin and student Client Components were importing Server Actions
from paths with dynamic segments ([courseId], [moduleId], [lessonId], [slug]).
This caused "Cannot access toStringTag on the server" RSC crash.
Consolidated all Server Actions into static files under src/lib/actions/:
- course-actions.ts (modules + enrollment)
- module-actions.ts (lessons + reorder + move)
- user-actions.ts (bulk grant / revoke)
- student-actions.ts (progress + homework + comments)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Server Actions imported from dynamic route paths ([courseId]/[moduleId]/[lessonId])
caused "Cannot access toStringTag on the server" crash after saving a lesson.
Moved saveLesson, saveHomework, deleteHomework to src/lib/actions/*.ts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
getSettings() and getSetting() now fall back to defaults when the
database is unavailable (e.g., during Docker image build).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Settings key-value table in Prisma with migration
- getSettings() / getSetting() helpers in lib/settings.ts
- Admin UI at /admin/settings with 6 sections: General, Notifications,
Student profile, Legal docs, Curator permissions, Code injection
- saveSettings() server action with admin-only guard
- Maintenance mode: non-admin users redirected to /maintenance page
- schoolName propagated to page metadata and all email templates
- headCode / bodyCode injected into root layout <head> and <body>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- All styles moved to inline attributes (no <style> block)
- Table-based layout for Gmail/Outlook/mobile client compatibility
- Aubade card effect via border-right/border-bottom:4px
- Monospace font stack with web-safe fallbacks
- btn() and quote() helper functions rewritten as <table> elements
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>
In Prisma 7, connection URL is managed via prisma.config.ts.
PrismaClient constructor no longer accepts log options directly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Prisma 7 no longer generates index.ts — entry point is client.ts.
Import changed from @/generated/prisma to @/generated/prisma/client.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix Prisma import: use @/generated/prisma alias (resolves Turbopack issue)
- Replace middleware.ts with proxy.ts (Next.js 16 convention)
- middleware function renamed to proxy
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>