Commit Graph
100 Commits
Author SHA1 Message Date
adminsandClaude Opus 5 20a25695bc Fix .md attachment upload in student questions
Windows reports an empty or generic MIME type for .md files, so the
type-first check rejected them with "Разрешены только jpg, png, pdf, md"
even though the extension was allowed — a client hit this while sending
us a vault note.

Extension is now the primary gate; MIME is validated only when the
browser actually sent a meaningful one. Generic types fall back to a
type derived from the extension so the file is served correctly later.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 10:27:14 +05:00
admins 005cd329b2 Allow image uploads in lesson comments
Add an image button to the comment toolbar backed by a new upload route
(images only, 5 MB, stored under comments/<userId>/), inserted as Markdown.

Only images served from our own storage are rendered: a third-party src would
be fetched by every reader of the lesson, exposing their address. Such links
are shown as plain links instead — this also defuses the external images a few
existing comments already carry.
2026-08-06 11:41:19 +05:00
admins 917253990b Drop react-markdown node prop from rendered comment links
The custom `a` renderer spread react-markdown's internal `node` prop onto
the anchor, leaking node="[object Object]" into the DOM on every comment link.
Destructure it out.
2026-07-31 19:48:31 +05:00
admins 2498d0f8da Enrich lesson comments: Markdown formatting, edit, higher limit
Add a formatting toolbar + safe Markdown rendering (react-markdown +
rehype-sanitize), raise the length limit 2000 -> 10000, and let authors
edit their own comments (new editComment action + editedAt column shown
as an 'изменено' badge).
2026-07-31 19:35:58 +05:00
adminsandClaude Fable 5 08e212365d Link homework review entry points to pending filter
Dashboard tiles and the curator CTA pointed to /curator/homework without
a status filter, so the default all-submissions list (newest first)
buried old PENDING items pages deep — 19 June submissions sat unseen
for 6 weeks while the top of the list showed freshly approved work.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 10:27:03 +05:00
admins 92fd88964e Accept buyer phone in grant API and enrich existing users
payment-router now forwards name/phone from the order. Store phone on
create; for existing users fill name (when it is an email placeholder)
and phone (when empty) without overwriting meaningful data. Also stop
dropping the parsed phone column in CSV import.
2026-07-11 19:41:49 +05:00
admins 33c0d3cafd compose: db на PG18 (тег + раскладка тома)
Прод LMS на Hoster.kz переведён с PostgreSQL 16 на 18 (09.07.2026,
dump/restore по регламенту major-upgrade-docker-бд). У PG18 другая
раскладка: том монтируется в /var/lib/postgresql, кластер лежит
в подкаталоге 18/docker.

Этот файл исполняется на сборочном сервере Hetzner, где том
lms-sb_postgres_data — hot-standby, уже переведённый на PG18 18.06.
То есть db-секция с postgres:16 + mount .../data была взведённой миной:
`docker compose -f docker-compose.prod.yml up -d` пересоздал бы standby
как PG16 поверх данных PG18 → initdb на непустом томе → рестарт-луп.
Ровно так легли lms-sb-db (01.07) и crm-prod (09.07).

Прод-compose Hoster (/root/lms-sb/docker-compose.yml) в git не живёт
и выровнен на месте; бэкап — /root/backups/major-upgrade-20260709/.
2026-07-09 17:28:27 +05:00
admins 5d94949810 Redirect large/non-inline lesson files to CDN instead of proxying
ZIP/DOCX etc. download natively — no need to force attachment; and the
157 MB Second Brain Vault archive must stream from Caddy with Range,
not through the app. Proxy attachment stays for small inline types (PDF/images).
2026-07-09 11:35:45 +05:00
admins 98cc023709 Force lesson materials to download as attachment with readable filename
Direct CDN links opened PDFs inline (no Content-Disposition), which reads
as 'can't download' behind popup blockers / on mobile. New proxy route
streams the file with attachment + human name; lesson link points to it.
2026-07-08 19:28:26 +05:00
admins 53caf54b03 Merge feature/clean-pdf: Чистый PDF tool (URL→PDF, Zotero, API) 2026-07-06 14:23:06 +05:00
admins 4734b99bea Document DNS-rebind residual and prod-enablement egress hardening
The security docs claimed the in-browser SSRF filter (context.route/
routeWebSocket) re-applies "the same filtering" as the pre-fetch DNS
check. That's inaccurate for hostnames: the browser-level filter only
blocks literal private IPs and localhost/.local/.internal suffixes —
it never re-resolves hostnames, so a same-hostname DNS-rebind (public
IP on first resolve, private IP on a later request from inside
browserless) is not closed at that layer. Correct the wording in the
design spec and TECHNICAL.md, and add a prominent note to both the
spec's deploy section and the plan's deploy notes: before flipping
TOOLBOX_VISIBLE on prod, harden the browserless container's network
egress (block 169.254.0.0/16 and RFC1918 ranges via host firewall or
a dedicated internal docker network) to close the residual at the
network layer. Also note that per-user limits currently count only
successful generations — failed renders are uncapped, a bounded
self-DoS risk worth a follow-up.
2026-07-06 13:47:00 +05:00
admins 118fa3961f Surface regenerate errors in clean-pdf Zotero section
regenerate() silently did nothing on {ok:false} or a rejected server
action, risking an unhandled promise rejection and leaving the student
staring at a stuck "меняем…" button with no feedback. Wrap the action
call in try/catch and show an inline error message on failure.
2026-07-06 13:45:12 +05:00
admins 1ccf994112 Strip active content from extracted HTML before PDF print
Defuddle-extracted article content is injected raw into the PDF HTML
and rendered by a real browser (browserless). As defense-in-depth
against a compromised or malicious source page, remove script/style/
iframe/object/embed elements and on* event-handler / javascript: href
attributes from the parsed DOM before serializing it into the template.
2026-07-06 13:44:41 +05:00
admins 02b16e311b Make Zotero script comment dynamic and assert valid JS
The generated script's setup comment hardcoded school.second-brain.ru
even though baseUrl is already interpolated elsewhere in the template,
so the comment would lie on any other host. Also add a regression test
that the generated script parses as valid JavaScript, to catch escaping
mistakes in the template literal.
2026-07-06 13:44:13 +05:00
admins f143f58cea Add browserless service to prod compose and document clean-pdf 2026-07-06 13:30:24 +05:00
admins fd40b1e394 Recompute Zotero script reactively on key regenerate 2026-07-06 13:13:19 +05:00
admins 0cbea10e1a Add clean-pdf tool page with web form and Zotero section 2026-07-06 12:49:06 +05:00
admins 668dd29397 Add regenerate action for clean-pdf API key 2026-07-06 12:44:29 +05:00
adminsandClaude Sonnet 5 9ad4d762d7 Wrap /api/pdf handler in try/catch for JSON error contract
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-06 12:39:01 +05:00
admins c50a295290 Add /api/pdf route with key/session auth and limits
Adds GET /api/pdf: resolves the caller via a Bearer sbpdf_ API key
(PdfApiKey table) or a Better Auth session cookie, then gates on paid
course access, burst/monthly usage limits, generates the PDF via
generateCleanPdf, records a ToolUsage row, and streams the file with
X-Uses-Count/X-Max-Uses headers. Errors map to 401/403/422/429/504
JSON responses. Whitelists /api/pdf in middleware PUBLIC_ROUTES so the
route can perform its own auth instead of being redirected to /login.
2026-07-06 12:24:23 +05:00
admins 0c56b0b809 Bound PDF print timeout and block WebSocket SSRF in clean-pdf 2026-07-06 12:21:24 +05:00
admins 89383fab1d Add PDF generation pipeline via browserless and Defuddle
Connects to browserless over CDP (playwright-core), extracts article
content with Defuddle/JSDOM, renders it through buildCleanHtml, and
prints a PDF on a second page. Adds an in-browser request filter as a
second line of SSRF defense against redirects to private/localhost
hosts, on top of assertPublicUrl's DNS check.

Integration test is gated by RUN_PDF_INTEGRATION=1 (describe.runIf) so
it is skipped in a normal npm run test and only runs against a real
browserless instance.
2026-07-06 12:09:28 +05:00
admins ef46cdbe29 Add paid-access and usage limit helpers for clean-pdf 2026-07-06 12:00:07 +05:00
admins b49680c118 Add PdfApiKey model and key storage helpers
Adds a Prisma model for per-student Clean PDF API keys plus a
hand-written migration (no local Postgres to run `migrate dev`
against). getOrCreatePdfKey/regenerateKey wrap the model with
lazy-creation and rotation logic on top of generatePdfKey().
2026-07-06 11:53:31 +05:00
admins 1d5b4f9251 Add API key generator and Zotero script builder 2026-07-06 11:46:31 +05:00
admins 6316c09993 Add PDF HTML template with typography, themes and TOC 2026-07-06 11:40:24 +05:00
admins 9a74339087 Block v4-mapped IPv6 literals in SSRF validator 2026-07-06 11:37:05 +05:00
admins 2c619be043 Add SSRF URL validator for clean-pdf 2026-07-06 11:28:11 +05:00
admins 52073fd914 Document clean-pdf env vars in .env.example 2026-07-06 11:25:31 +05:00
admins f5768331cf Ignore .superpowers SDD scratch dir 2026-07-06 11:21:01 +05:00
admins 541853bb1d Add clean-pdf dependencies and browserless dev service 2026-07-06 11:19:51 +05:00
adminsandClaude Fable 5 e795c4d655 Adapt clean-pdf plan to staging-based verification (no local Docker)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 11:13:22 +05:00
adminsandClaude Fable 5 aaf84ea931 Add clean-pdf implementation plan
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 10:10:11 +05:00
adminsandClaude Fable 5 16b17685f6 Ignore lms-auth.json auth state
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 09:57:08 +05:00
adminsandClaude Fable 5 5b81a7b594 Add Clean PDF tool design spec
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 09:55:46 +05:00
adminsandClaude Fable 5 980c4d621e Use bullet marker in welcome scheme cards
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 09:43:38 +05:00
adminsandClaude Fable 5 8c2bd42be3 Let welcome header wrap on narrow screens
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 09:37:56 +05:00
adminsandClaude Fable 5 fa7c25465c Add public DS-2 welcome page on root for unauthenticated visitors
Second-brain scheme (logo + six cards with rays) reworked from the old
platform's infographic. Root stays role-redirecting for signed-in users;
middleware opens "/" as an exact match only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 09:32:49 +05:00
adminsandClaude Fable 5 1133557a62 Sync dev compose with Hetzner hot-standby: postgres 18, loopback port, versioned volume
Change was made live on the build server on 20260701 and is now captured
in git per the no-drift rule.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 16:37:39 +05:00
adminsandClaude Fable 5 a6835567f1 Open checkout modal from locked course cards on dashboard
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 16:35:30 +05:00
adminsandClaude Fable 5 c2f84079ac Add DS-2 checkout modal for locked courses
Form POSTs directly to payment-router /pay/order with the student's
email prefilled, so the purchase auto-attaches to the existing account.
Payment methods are fetched from /pay/methods with a robokassa fallback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 16:35:30 +05:00
adminsandClaude Fable 5 f8f246ca9b Add store catalog config for dashboard upsell checkout
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 16:35:30 +05:00
adminsandClaude Opus 4.8 d2094c9541 Make URLs in homework answers and feedback clickable
Apply linkify() to student submission text (curator review page + student
cabinet: reviewed/approved/pending) and to curator feedback text, so pasted
links (e.g. Yandex.Disk) render as clickable anchors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 19:29:21 +05:00
adminsandClaude Opus 4.8 7c8e72cd94 Add email notification on new lesson comment
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>
2026-07-01 10:02:07 +05:00
adminsandClaude Opus 4.8 d20e277535 WS6 fix: SQL пометить справочным — курсы создавать через Admin UI (CUID невалидны для psql -f)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 10:08:30 +05:00
adminsandClaude Opus 4.8 47e301a5b8 feat(ws6): SQL-скрипт создания курсов-носителей biz-brain + verify-скрипт
- scripts/create_biz_brain_courses.sql: идемпотентное создание 5 курсов
  (biz-brain-base, biz-brain-premium, module-caldav, module-ai-pdf, module-legacy)
- scripts/verify_biz_brain_grant.sh: проверка /api/internal/grant (Нужен Дмитрий для запуска на проде)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 19:27:22 +05:00
adminsandClaude Opus 4.8 39e2102a18 Gate Obsidian Toolbox behind TOOLBOX_VISIBLE flag
Toolbox is still being refined; hide it from clients on prod while keeping it
usable on staging. Entry points (header link, dashboard card) render only when
TOOLBOX_VISIBLE=true; a tools/layout redirects /tools/* to /dashboard otherwise.
Routes and ToolUsage table are untouched (no destructive migration).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 12:15:42 +05:00
adminsandClaude Opus 4.8 3e2c70dd51 Fix global CSS wipe: inject head/body code via DOM, not <head dangerouslySetInnerHTML>
Rendering <head dangerouslySetInnerHTML={headCode}> in the App Router root
layout let the manual <head> overwrite the React-hoisted <link rel=stylesheet>
during hydration — all Tailwind styles dropped for real users once JS ran
(headless snapshots taken before hydration looked fine). headCode held the
Yandex.Metrika snippet. Replace with HtmlInjector client component that appends
the code through the DOM post-hydration, recreating <script> nodes so they run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 12:13:58 +05:00
adminsandClaude Opus 4.8 a239607360 Show homework badge by review status, not just feedback presence
Curator list labeled APPROVED-without-feedback submissions as 'Без ответа'
(badge keyed on feedbacks only). Key it on status: Принято / С отзывом /
На рассмотрении / Отклонено / Без ответа. Dashboards already count by status.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 10:23:35 +05:00
adminsandClaude Opus 4.8 6317dade9e Add Dataview and Bases generators (Toolbox Phase 2)
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>
2026-06-24 10:04:09 +05:00
adminsandClaude Opus 4.8 1aa99b9def Add Toolbox promo card to student dashboard (Мои курсы)
Entry point into /tools alongside courses, per gate #3 decision.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 16:00:58 +05:00
adminsandClaude Opus 4.8 b6d555ab3b Add Obsidian Toolbox — 4 syntax generators under /tools
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>
2026-06-23 15:45:52 +05:00
adminsandClaude Opus 4.8 f8aa27533c Add vault graph poster to Obsidian Wrapped
- parse.worker collects edges, builds top-150-by-degree graph (links capped 600)
- graphPoster.ts renders force-graph on off-screen canvas -> PNG (DS-2 colors),
  node names never leave the browser
- WrappedClient: Карточка/Граф toggle, optional node labels, lazy-load force-graph
- THIRD-PARTY-NOTICES for force-graph (MIT) and next/og satori/resvg (MPL-2.0)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:57:36 +05:00
adminsandClaude Opus 4.8 0a1425301d Add Wrapped link to student nav
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:25:48 +05:00
adminsandClaude Opus 4.8 06b8d9f64b Add Obsidian Wrapped feature (inside-LMS, client-side vault parsing)
- WrappedRun Prisma model + migration (aggregates only, no note names/text)
- /wrapped authed route: FSA + webkitdirectory + drag-drop reader, Web Worker parser
- archetype + score heuristics, ДС-2 result card
- POST /api/wrapped/run (userId from session), public /share/wrapped/[token]
- next/og OG image (Satori+resvg-wasm, alpine-safe) + Fira Mono ttf
- middleware: /share/wrapped public

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:19:59 +05:00
adminsandClaude Opus 4.8 1cde567dbe Add NEXT_PUBLIC_METRIKA_COUNTER_ID build-arg to Dockerfile
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 12:31:11 +05:00
adminsandClaude Opus 4.8 0e67a53122 Add Яндекс.Метрика funnel goals: signup_free + gate_view (env-driven counter)
- window.ym reachGoal 'signup_free' on registration success
- GateViewTracker client component fires 'gate_view' on tripwire gate mount
- counter id via NEXT_PUBLIC_METRIKA_COUNTER_ID (build-time)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 12:13:32 +05:00
adminsandClaude Opus 4.8 7bf3935c81 Add freemium variant C: archetype persistence, dashboard banner, lesson gate
- User.archetype/utmSource/utmMedium/utmCampaign fields + migration
- register flow reads ?archetype/?utm_* and saves them on the User row
- dashboard ArchetypeBanner: per-archetype 'твой путь' block
- lesson gate after wow-moment lesson (WOW_MOMENT_LESSON_ID=obs-start-l2-006) → tripwire + full-course offer

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 11:26:34 +05:00
adminsandClaude Sonnet 4.6 bb6c806cdd Add quiz-lead internal endpoint for Typebot → Listmonk funnel
POST /api/internal/quiz-lead accepts archetype quiz results and
idempotently upserts subscriber in Listmonk (create on 201, PUT on 409).
Documents QUIZ_LEAD_SECRET and LISTMONK_* vars in .env.example.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-22 14:34:39 +05:00
adminsandClaude Opus 4.8 9fbb7aea6c Accept CDN URLs for question attachments
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>
2026-06-20 15:11:36 +05:00
adminsandClaude Opus 4.8 2436f77de5 Add 'approve without feedback' button for homework review
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>
2026-06-19 16:10:29 +05:00
adminsandClaude Opus 4.8 a9869bfac8 Backlog: markdown rendering for homework descriptions
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 10:15:27 +05:00
adminsandClaude Opus 4.8 4b300f3466 Merge branch 'feat/freemium-auto-enroll' into main
Auto-enroll new signups into free course obsidian-start (freemium)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 16:18:03 +05:00
adminsandClaude Opus 4.8 a8cd2cd4d4 feat: auto-enroll new signups into free course obsidian-start (freemium)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 14:05:52 +05:00
adminsandClaude Opus 4.8 2ea9c6fda0 Don't force password change while admin is impersonating
The mustChangePassword guard redirected an impersonating admin to
/change-password, which sits outside the student layout (no
return-to-admin banner) — a dead end. Skip the guard when impersonating,
and add the stop-impersonate banner to the change-password page as a
fallback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 11:03:58 +05:00
adminsandClaude Opus 4.8 ec011ab7fd Add support mailto on login for missing password-reset email
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 12:58:28 +05:00
adminsandClaude Opus 4.8 27027b134f Use CDN URL for public file links when S3_CDN_URL is set
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>
2026-06-16 12:46:31 +05:00
adminsandClaude Opus 4.8 04e8377dab Show hint on lessons with pending homework or quiz
Lessons with homework/quiz have no manual complete button — they
auto-complete on submission. Before submission nothing was shown,
leaving students unsure how to progress. Add a muted hint.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 12:22:44 +05:00
adminsandClaude Opus 4.8 5d4630519c Force download for lesson materials (Content-Disposition: attachment)
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>
2026-06-15 16:06:51 +05:00
adminsandClaude Opus 4.8 a7abf454d9 Add internal grant endpoint + temp-password / force-change flow
- /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>
2026-06-14 16:25:35 +05:00
adminsandClaude Opus 4.8 5a67ac8086 Close captcha-bypass and email-abuse vectors
- 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>
2026-06-12 14:18:30 +05:00
adminsandClaude Opus 4.8 85cf8be705 Explicitly resend verification email on unverified login
Better Auth does not auto-resend on 403 (verified against Resend logs),
so request a fresh link via authClient.sendVerificationEmail before
telling the user a new email was sent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 14:05:32 +05:00
adminsandClaude Opus 4.8 2790a7d24b Wire up email verification flow for new signups
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>
2026-06-12 14:01:46 +05:00
adminsandClaude Opus 4.8 619ae393bd Allow /api/register through auth middleware
The Turnstile/honeypot wrapper /api/register was missing from
PUBLIC_ROUTES, so unauthenticated POSTs were redirected to /login (307).
The register form followed the redirect, got HTML, and failed to parse
it — surfacing as "connection error". This silently broke signups for
new users. Add /api/register alongside /api/auth.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 13:26:19 +05:00
adminsandClaude Opus 4.8 8240db5d22 Make registration errors informative
- 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>
2026-06-12 13:20:56 +05:00
adminsandClaude Opus 4.8 05f46ec830 Add "login as user" to user profile card
Extract ImpersonateButton into a shared component and reuse it on the
admin user profile page (header, hidden for admins) — same behaviour as
the users list.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 15:55:57 +05:00
adminsandClaude Opus 4.8 3b8a1ce7ee Link question author name to their admin profile
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 10:53:01 +05:00
adminsandClaude Opus 4.8 a290b9495c Enroll modal: days input instead of date; hide bundle if base course owned
- Quick-enroll modal now takes access duration in days (0/empty = unlimited)
  and shows the computed expiry date, instead of a manual date picker.
- Dashboard upsell no longer shows the "Всё включено" bundle of a course
  the student already owns in its base edition (obsidian/zotero).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 16:13:15 +05:00
adminsandClaude Opus 4.8 8c9f1e0a55 Add mobile spacer so admin content clears the hamburger button
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 10:01:58 +05:00
adminsandClaude Opus 4.8 ffc5905309 Make admin/curator sidebar collapsible on mobile
The admin shell sidebar was always fixed at 208px with no mobile
handling, so on phones it covered half the screen with no way to hide
it. Add a hamburger toggle + overlay (mirroring the student course
sidebar): off-canvas on <lg, sticky as before on desktop.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 09:58:15 +05:00
adminsandClaude Opus 4.8 3ed5540f40 Reduce blur on locked course cards for readability
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 16:06:32 +05:00
adminsandClaude Opus 4.8 7dc3412903 Add locked course cards (upsell) to student dashboard
Show the sellable course lineup on the dashboard: courses the student
doesn't own render as blurred cards with a lock and a CTA linking to the
product landing page (new tab). Owned courses are unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 16:01:39 +05:00
admins 7c85a7835b Widen dashboard course cards and enlarge title/description text 2026-06-04 10:32:30 +05:00
admins ecf48de7e8 Copy code block verbatim (fences live in content for Obsidian snippets) 2026-06-03 15:34:15 +05:00
admins ecbca89cef Copy full markdown fence for language-tagged code blocks (dataview/query) 2026-06-03 15:23:27 +05:00
admins cf942d223e Mark lesson complete on homework submit; add copy button + styling to code blocks 2026-06-03 14:58:30 +05:00
admins adc1d6fc06 Loosen sign-in rate limit to 10/min per IP for migration onboarding 2026-06-03 14:03:11 +05:00
admins 6624a0c535 Restore list markers (disc/decimal) for lesson content lists 2026-06-03 13:34:11 +05:00
admins a65125a239 Linkify URLs and preserve line breaks in question messages 2026-05-27 14:16:25 +05:00
admins 14b1eacf0d Switch homework pending filter from feedbacks-none to status-based (PENDING/REVIEWING) 2026-05-27 11:59:25 +05:00
admins d9e3531c49 Apply lesson cover as poster for embedded kinescopeVideo nodes 2026-05-26 16:40:47 +05:00
admins 5e5f616888 debug: move banner outside kinescopeId block to catch null ID 2026-05-26 16:22:18 +05:00
admins 4222834539 temp: visible debug banner for coverImage 2026-05-26 16:11:33 +05:00
admins 2525ca2b32 temp: debug coverImage value in lesson page 2026-05-26 15:49:47 +05:00
adminsandClaude Sonnet 4.6 cfd3681979 Fix poster overlay hidden behind Kinescope iframe
Kinescope renders an iframe that ignores z-index stacking.
Switch to mutually-exclusive render: show poster image OR
the player, never both. Click on poster dismisses it and
reveals the player.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 15:40:23 +05:00
adminsandClaude Sonnet 4.6 c2c2190cb6 Show custom lesson cover as video poster overlay
Kinescope React player ignores the poster prop. Replace with a
click-to-dismiss overlay rendered above the player: shows the
lesson coverImage, a play button, and on click hides itself
and calls player.play() via ref.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 15:35:39 +05:00
adminsandClaude Sonnet 4.6 6542f9b6b6 Add NEXT_PUBLIC_TURNSTILE_SITE_KEY build arg to Dockerfile
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 12:39:46 +05:00
adminsandClaude Sonnet 4.6 231ed083be Add anti-bot registration protection
- Custom /api/register route: honeypot, disposable email blocking, Cloudflare Turnstile verification
- Register form: honeypot hidden field, Turnstile widget (loads when NEXT_PUBLIC_TURNSTILE_SITE_KEY set)
- Install disposable-email-domains package
- Add NEXT_PUBLIC_TURNSTILE_SITE_KEY and TURNSTILE_SECRET_KEY env vars to .env.example

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 12:35:06 +05:00
adminsandClaude Sonnet 4.6 c1c33a0619 Make links in lesson content visually distinct (underline + bold)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 11:35:07 +05:00
adminsandClaude Sonnet 4.6 7e6ad8067c Add in-content video embed and image replace/delete controls
- KinescopeVideo TipTap node: insert video anywhere in lesson content via toolbar button; editor shows ID preview, student view renders KinescopePlayer
- ImageWithControls NodeView: hover over any image in editor to reveal Replace and Delete buttons

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 11:27:52 +05:00