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>
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>
- Backend: explicitly return 409 EMAIL_TAKEN when the email already
exists, instead of Better Auth's silent 200 (common after migration).
- Frontend: split error handling — network failure, non-JSON body
(502/proxy), and meaningful server errors get distinct messages; the
"email already registered" case shows login / reset-password links
instead of a generic "connection error".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Replace non-existent --surface/--surface-muted/--border-strong with actual
design-system tokens (--color-surface, --background, --foreground, --muted)
- Remove tmp/ segment from S3 upload key in question-upload route
- Add auto-scroll to bottom on new message in QuestionThread
- Block Send while file upload is in progress (uploading guard)
- Replace <a> with Next.js <Link> in new question page back-link
- Replace hardcoded #c00 error color with var(--destructive) in both files
- Replace hardcoded #E8E8E0/#F5F5F0 hex backgrounds with CSS tokens
- 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>
Implements GET/POST /api/questions, GET /api/questions/[id] with read tracking, POST /api/questions/[id]/messages with email notifications, PATCH /api/questions/[id]/close for staff, and POST /api/student/question-upload for file attachments.
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
POST /api/admin/lesson-files now checks for an existing record with the
same (lessonId, name) before uploading — replaces it (old S3 object
deleted) instead of always creating a new one. Previously every save
cycle accumulated an extra copy; 1183 duplicates occupying 6.5 GiB were
found and cleaned up.
DELETE now receives the file URL and extracts the S3 key from it, so
manual deletion actually removes the object from storage.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Store human-readable label in LessonFile.name via optional label field on upload
- Add PATCH endpoint to rename existing files inline
- Admin: label input before upload, click-to-edit inline rename
- Student: colored format badge (PDF/DOCX/XLSX/ZIP/etc) replaces paperclip emoji
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Course: add allowAudio toggle (per-course setting, off by default)
- HomeworkSubmission: add audioUrl field
- Student: AudioRecorder in homework form when allowAudio is enabled
- Student: show audio player in submission view and curator feedback view
- Curator: show student audio on submission detail page
- AudioRecorder: accept uploadUrl prop (reused for student/curator)
- API: /api/student/audio-upload route for S3 upload
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Admin:
- HomeworkEditor in lesson page: create/update/delete assignment description
Student:
- HomeworkSection in lesson page: view assignment, submit text + files
- Resubmission allowed until curator gives feedback
- Shows feedback from curator with date and name
Curator:
- New layout with Second Brain dark sidebar (replaces green theme)
- /curator/dashboard: stats cards (pending, total, reviewed this week)
- /curator/homework: list of all submissions, pending highlighted
- /curator/homework/[id]: review submission, write feedback, redirect after send
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>