| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
fix(auth): stop unexpected sign-outs from transient refresh failures Sessions were being ended by conditions that say nothing about whether the user is still signed in: - The auth proxy cleared every auth cookie on any non-ok refresh response, including 429 and 5xx. A brief backend hiccup destroyed an httpOnly refresh cookie that still had weeks of validity, with no way back except signing in again. Only 401/403 end a session now, and the client mirrors that distinction instead of collapsing every failure into "logged out". - The refresh rate limit was 60/min per IP. The endpoint is cookie-authenticated, so there is nothing to brute-force, and many users legitimately share one address. Raised to 600/min. - getServerSession() called the rotating refresh endpoint from Server Components, which cannot set cookies, so it consumed the one-time-use token and discarded the replacement. The browser then re-presented a consumed token, which is treated as theft and revokes every session on every device. Server-side session resolution no longer refreshes; the client restores the session through the route handler that can persist the rotation. - The benign-replay grace window was 30s, too short for a suspended laptop, a background tab, or a slow connection. Now 5 minutes; reuse hours or days later is still caught. Also makes the inactivity window explicit and configurable via LEARNHOUSE_AUTH_REFRESH_TOKEN_DAYS (default 30), clamped to a 14-day floor so it cannot be misconfigured below two weeks. | 2 个月前 | |
fix: sort folders by org mode and hide certificate copy Folder content sorted by position only, ignoring org sort_mode, so dashboard drag order didn't match the public library page. Certificate copy was shown based on the learner's certificate row rather than whether the course has certification enabled, so uncertified courses displayed misleading "no certificate" warnings. Shared sort comparator: apps/web/lib/library/sort.ts. New gate: useCourseCertification hook. | 1 个月前 | |
Add Folders & Media library, drop Collections, configurable public menu Replaces the flat Collections grouping with a Drive-like library: nested folders that hold courses plus references to podcasts, communities, boards, playgrounds and a new Media resource (uploaded files or embeds). Root behaves like Drive too — loose content sits alongside folders. Backend - New folder, foldercontent and media tables (+ Alembic migration) and a migration that drops collection / collectioncourse. - Folders & media wired into the config-driven RBAC layer, so public flag, authorship, admin role and usergroup membership all gate them with no per-resource code. - Whole-tree library search with folder-path context; item counts on folders. - Media uploads now accept MS Office (docx/xlsx/pptx + legacy doc/xls/ppt) and zip alongside images/video/audio/pdf. All validated by magic bytes, with a zip-bomb guard, SVG still blocked, and the stored extension derived from the validated content type rather than the client filename. - Org config gains a menu section with its own PUT endpoint. Frontend - Library section on the dashboard and a read-only public /library, sharing the same folder/course/media cards. Manage-access popover on folders and course cards, folder colour + cover customisation, share links. - Media previews everywhere: og:image (via link-preview) then screenshot for embeds, first-page render for PDFs, first frame for video, the image itself for images. - New "Menu" tab in org settings to toggle, reorder, rename and add public nav links, with a Phosphor icon picker for custom links and plan/feature gating shown inline. Removed the org-settings Features tab and every link to it; features are now plan-driven only. | 2 个月前 | |
feat(branding): retire the Unsplash picker and use duotone icons Drop the Unsplash image picker from every upload surface (course, podcast, community, board and playground thumbnails, the editor image block, the sign-in background) and from the CLI setup flow, along with the unsplash-js dependency and its access key. Existing content and sign-in backgrounds that were picked from Unsplash keep rendering with their attribution; the picker is simply no longer offered. AI-generated backgrounds now upload as a custom photo instead of being stored as a remote URL. Switch the branding page's phosphor icons to the duotone weight, and cover the square logo upload wrapper. Also refresh apps/api/uv.lock, which the release bump left one version behind. | 13 天前 | |
fix(feature-gate): let admins re-enable AI after disabling it Turning off "Enable AI Features" in Org Settings -> AI trapped admins on a "Feature Disabled" card with no way back: the toggle lived inside FeatureGate and vanished along with everything else it gated. On self-hosted the only way out was hitting the config API directly. FeatureGate now takes an opt-in allowWhenDisabled prop that suppresses just the admin-disabled card; plan gating still shows the upgrade card as before. OrgEditAI opts in and shows a notice when AI is off instead of hiding the switch. Gate branching moved into a pure resolveGateReason() so it's testable under bun, since apps/web tests have no React runtime. Also stopped useResolvedFeature from applying its plan fallback while the org is still loading: it was firing upgrade-shown analytics with a guessed plan before real entitlements arrived, including on public org pages. Added feature-gate-lockout.test.mjs (15 tests) and one i18n key in en.json/ar.json. eslint and tsc clean on changed files; not tested in a live browser. Fixes #1061 | 24 天前 | |
fix: sort folders by org mode and hide certificate copy Folder content sorted by position only, ignoring org sort_mode, so dashboard drag order didn't match the public library page. Certificate copy was shown based on the learner's certificate row rather than whether the course has certification enabled, so uncertified courses displayed misleading "no certificate" warnings. Shared sort comparator: apps/web/lib/library/sort.ts. New gate: useCourseCertification hook. | 1 个月前 | |
feat(editor,assignments): add H5P size modes and optional due dates - H5P blocks get a size picker (Auto/16:9/4:3/Short/Medium/Tall) plus a manual drag handle, so a clipped embed no longer needs fullscreen to be readable. - Assignment due_date is now nullable across the migration, service layer, and both web forms; an explicit null clears an existing deadline instead of being ignored. - Hoist the Arabic key set out of rtl-guard's filter loop, cutting that test from ~5.2s to ~0.4s. | 24 天前 | |
feat(users): per-student audit & analytics Add an Analytics tab under Users with a complete, exportable per-student learning record for legal/compliance needs. Backend: - New append-only user_audit_event table (real timestamptz, IP, user-agent) as the durable source of truth, plus record_audit_event() writing in an isolated committed session. - Emit at student-action sites: login (password + SSO), enroll, activity/ course completion, assignment submit + grade received, certificate claim. - Dossier service unions durable Postgres records + audit log + Tinybird behavioral data; /audit endpoints (user dossier, multi-user summary, CSV/JSON export) gated by org-admin RBAC + Enterprise plan. Frontend: - Analytics tab, searchable student list with multi-select, dedicated per-student page + quick-view modal, sectioned dossier (connections, progress, assignments, code, community, certificates, behavior), comparison table, and CSV/JSON/PDF export. | 1 个月前 | |
feat(quiz): explicit single- and multiple-response questions The quiz builder had no way to say whether a question is pick-one or select-all-that-apply. Storage already allowed several correct options and the grader already did an exact-set match, so multi-answer worked by accident: nothing enforced single-select when authoring, and the learner got no affordance telling the two apart. - Questions carry response_type (single|multiple) and tasks carry grading_mode (all_or_nothing|partial_credit). Both live in the opaque contents JSON, so there is no migration. - Content authored before this has neither field. resolve_response_type is the single place inference lives: 2+ correct options was always a de-facto select-all. Stored questions are never rewritten, and an unrecognised value falls back to inference so a typo cannot grade a 3-correct question as a pick-one. - Single-response marks and selects like a radio group; multiple keeps toggling and says so. Both get role/aria-checked and keyboard activation, and a circle-vs-square indicator. - Partial credit awards (correct - incorrect) / total_correct, clamped, so selecting everything scores 0 rather than full marks. Default stays all-or-nothing, which reduces to the previous formula exactly. - The server grader and the learner's preview grader run the same shared helper on each side, since a divergence there shows one score and stores another. - _strip_answer_key stamps the resolved response_type on the outgoing copy: the learner needs the affordance, and the key it would otherwise be inferred from has just been removed. | 1 个月前 | |
feat: fix multiple issues | 4 个月前 | |
fix(web): make shared links absolute Links that leave the app were built with `getUriWithOrg`, which returns a relative path whenever navigation would stay on the current origin. That is correct for in-app navigation but wrong for anything copied, scanned, or consumed by another system: the host is missing. Adds `getAbsoluteUriWithOrg` plus a hydration-safe `useAbsoluteUrl` hook and uses them for the links that are handed to the outside world: - Invite/signup link: copying it yielded `/signup?inviteCode=...`. - Certificate QR codes (course end + verification page): encoded a relative path, so scanning the certificate led nowhere. - Podcast RSS feed URL shown in the dashboard, pasted into Apple/Spotify. - Sitemap and robots URLs handed to Search Console. - The RSS feed route itself, whose <link>/<atom:link> were relative in single tenancy (an invalid feed); it now derives the origin from the request host. | 1 个月前 | |
feat: add podcasts distribution + podcasts blocks | 7 个月前 | |
fix(web): auth session/cookie hardening — refresh grace, logout epoch, anonymous-401 guard, custom-domain Turnstile gate | 2 个月前 | |
feat(i18n): resolve text direction from the active locale Adds the plumbing that makes direction a property of the language rather than something hardcoded, plus the codemod used to convert the codebase. Direction has to be resolved on the client. i18next's highest-priority detection source is localStorage, which the server cannot read, and reading the cookie in the root layout would deopt every page to dynamic rendering and still be wrong whenever the two disagree. So dir-init.js runs synchronously in <head> and writes lang/dir before the body paints — the same approach embed-bg.js already uses to avoid a flash. Its detection order mirrors lib/i18n.ts exactly, and the RTL list it carries is asserted against lib/direction.ts by a test, since it's a constant duplicated into a file the bundler never sees. AVAILABLE_LANGUAGES gains a `dir` field, so Persian works today and a future locale is a one-line change; nothing branches on "is Arabic". useDirection() exposes it to components along with a multiplier for physical offsets, and Radix's DirectionProvider makes align="end", collision flipping and arrow-key navigation direction-aware across ~40 call sites with no per-component edits. The codemod converts physical Tailwind utilities to logical ones. It parses with the TypeScript compiler API rather than running a regex over the file, because `pr-`, `mr-` and `pl-` appear in URLs, i18n keys and identifiers. space-x-*, divide-x* and inset-x-* are skipped — they already emit logical properties in v4 — and so is any 50% inset, which is a centering idiom whose paired translate may live in a sibling class or an inline style, and which lands off-centre in RTL if converted alone. Also drops tailwind.config.js. Tailwind v4 only loads it via an @config directive this project doesn't have, so it was dead — and a trap for anyone later adding an RTL plugin there. | 1 个月前 | |
fix(security): make the superadmin surface Enterprise-only The superadmin dashboard shipped in OSS builds. Its backend routes are Enterprise-only, so what an OSS operator got was admin chrome over endpoints that were not there, plus a login form with nothing behind it. The only check that said "not available in OSS" ran client-side, on a signal that cannot be trusted. Add a shared guard in security/superadmin.py that returns 403 with a machine-readable ee_required detail. It is applied to require_superadmin, the three AI-credit mutators, and superadmin token authentication. The read-only AI-credits endpoint is deliberately left alone — org billing and usage pages read it in every mode. On the web side, app/admin/layout.tsx now decides server-side and renders a licence screen before the admin providers mount. Being a root layout it also covers /admin/login, which sat outside the gated route group. The untrusted client-side check is removed rather than kept alongside the new one, so there is a single gate with one failure direction. EELicenseError gains a detector for the new 403, kept distinct from the existing 503 case, which means something different and is worth retrying. Also tightens deployment-mode resolution to fail closed, and fixes a loading-state bug in SuperadminAuthorization where logged-out visitors never reached the right screen. | 1 个月前 | |
feat: refactor file upload functionality to include security validation and improve file handling across various services | 10 个月前 | |
feat(orgs): add font configuration support for organizations | 5 个月前 | |
i18n(ar): complete the Arabic locale and localize dates and prices Arabic now has full key coverage: 0 missing, and the 20 values still matching English are brand names and URLs. Getting there took three passes, because the gaps had three different causes. 305 keys existed but held untranslated English — the entire Library feature among them, which is why it read as completely untranslated. Another 221 were absent from en.json altogether: the code called t() with a key no locale had, so i18next fell through to the English defaultValue in the call and rendered that forever, in every language. Those are added to en.json too, so the other twenty locales can now translate them. And the onboarding sidebar rendered step titles straight from the English step definitions instead of through t(), while its sibling component did the same lookup correctly. Dates and numbers move to lib/format, which loads the dayjs locale centrally. That fixes a bug predating this branch: all twelve dayjs call sites extended relativeTime locally and none imported a locale, so "2 hours ago" was English in all 22 languages. Several call sites also hardcoded 'en-US', assignment timestamps used 'en-UK' — not a valid tag, so it silently fell back to the browser default — and the thumbnails branched on `i18n.language === 'fr' ? 'fr-FR' : 'en-US'`, giving French dates to French and English to everyone else. Numerals are pinned to Latin digits and the Gregorian calendar for Arabic and Persian, behind one function. Intl.NumberFormat('ar') resolves to Arabic-Indic digits on most ICU builds and ar-SA defaults to the Hijri calendar; neither suits a screen where prices and analytics sit next to Latin-digit values from the API, and Arabic regions are split on this anyway. Month names and relative times still localize. Also translates the sixteen aria-labels in the dashboard navigation, which were the last hardcoded strings in those files — screen reader users were hearing English labels in every language. | 1 个月前 | |
feat(orgs): add font configuration support for organizations | 5 个月前 | |
i18n(ar): complete the Arabic locale and localize dates and prices Arabic now has full key coverage: 0 missing, and the 20 values still matching English are brand names and URLs. Getting there took three passes, because the gaps had three different causes. 305 keys existed but held untranslated English — the entire Library feature among them, which is why it read as completely untranslated. Another 221 were absent from en.json altogether: the code called t() with a key no locale had, so i18next fell through to the English defaultValue in the call and rendered that forever, in every language. Those are added to en.json too, so the other twenty locales can now translate them. And the onboarding sidebar rendered step titles straight from the English step definitions instead of through t(), while its sibling component did the same lookup correctly. Dates and numbers move to lib/format, which loads the dayjs locale centrally. That fixes a bug predating this branch: all twelve dayjs call sites extended relativeTime locally and none imported a locale, so "2 hours ago" was English in all 22 languages. Several call sites also hardcoded 'en-US', assignment timestamps used 'en-UK' — not a valid tag, so it silently fell back to the browser default — and the thumbnails branched on `i18n.language === 'fr' ? 'fr-FR' : 'en-US'`, giving French dates to French and English to everyone else. Numerals are pinned to Latin digits and the Gregorian calendar for Arabic and Persian, behind one function. Intl.NumberFormat('ar') resolves to Arabic-Indic digits on most ICU builds and ar-SA defaults to the Hijri calendar; neither suits a screen where prices and analytics sit next to Latin-digit values from the API, and Arabic regions are split on this anyway. Month names and relative times still localize. Also translates the sixteen aria-labels in the dashboard navigation, which were the last hardcoded strings in those files — screen reader users were hearing English labels in every language. | 1 个月前 | |
feat(i18n): resolve text direction from the active locale Adds the plumbing that makes direction a property of the language rather than something hardcoded, plus the codemod used to convert the codebase. Direction has to be resolved on the client. i18next's highest-priority detection source is localStorage, which the server cannot read, and reading the cookie in the root layout would deopt every page to dynamic rendering and still be wrong whenever the two disagree. So dir-init.js runs synchronously in <head> and writes lang/dir before the body paints — the same approach embed-bg.js already uses to avoid a flash. Its detection order mirrors lib/i18n.ts exactly, and the RTL list it carries is asserted against lib/direction.ts by a test, since it's a constant duplicated into a file the bundler never sees. AVAILABLE_LANGUAGES gains a `dir` field, so Persian works today and a future locale is a one-line change; nothing branches on "is Arabic". useDirection() exposes it to components along with a multiplier for physical offsets, and Radix's DirectionProvider makes align="end", collision flipping and arrow-key navigation direction-aware across ~40 call sites with no per-component edits. The codemod converts physical Tailwind utilities to logical ones. It parses with the TypeScript compiler API rather than running a regex over the file, because `pr-`, `mr-` and `pl-` appear in URLs, i18n keys and identifiers. space-x-*, divide-x* and inset-x-* are skipped — they already emit logical properties in v4 — and so is any 50% inset, which is a centering idiom whose paired translate may live in a sibling class or an inline style, and which lands off-centre in RTL if converted alone. Also drops tailwind.config.js. Tailwind v4 only loads it via an @config directive this project doesn't have, so it was dead — and a trap for anyone later adding an RTL plugin there. | 1 个月前 | |
fix(web): auth session/cookie hardening — refresh grace, logout epoch, anonymous-401 guard, custom-domain Turnstile gate | 2 个月前 | |
fix(web): auth session/cookie hardening — refresh grace, logout epoch, anonymous-401 guard, custom-domain Turnstile gate | 2 个月前 | |
feat: implement script edition and loading on the frontend | 1 年前 | |
fix: update z-index values for popover in z-index.ts and globals.css | 6 个月前 |
| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
| 2 个月前 | ||
| 1 个月前 | ||
| 2 个月前 | ||
| 13 天前 | ||
| 24 天前 | ||
| 1 个月前 | ||
| 24 天前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 4 个月前 | ||
| 1 个月前 | ||
| 7 个月前 | ||
| 2 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 10 个月前 | ||
| 5 个月前 | ||
| 1 个月前 | ||
| 5 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 1 年前 | ||
| 6 个月前 |