Skip to content

Phase 6: Canvas integration — connect, link, file import, roster sync - #82

Open
sroygit wants to merge 10 commits into
mainfrom
saurav/canvas-integration
Open

Phase 6: Canvas integration — connect, link, file import, roster sync#82
sroygit wants to merge 10 commits into
mainfrom
saurav/canvas-integration

Conversation

@sroygit

@sroygit sroygit commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

Phase 6 — Canvas integration (read-only)

An instructor connects their own Canvas account via OAuth, links a Canvas course they teach, imports Canvas Files into the course's materials, and syncs the Canvas roster so a student on it can enroll with the registration code alone.

Design: docs/superpowers/specs/2026-08-27-canvas-integration-design.md · Plan: docs/superpowers/plans/phase-6/Saurav/ · Contract: docs/api-contract.md → "Canvas LMS".

What's in it

  • @ubc/ubc-genai-toolkit-lms-integration@^1.2.0 (GitHub Packages). The committed .npmrc carries only the @ubc registry line; each developer needs a read:packages PAT in ~/.npmrc.
  • components/lms/ binds the package to FinanceBot identity: getUserKey = req.user.puid, tokens in lmsCanvasTokens. Mounted under /api/lms/canvas only when all four CANVAS_* variables are set.
  • Course linkcourse.canvas sub-document. PUT link refuses any Canvas course not in the connected identity's teacher list. Every Canvas read derives its external course id from the stored link, never a request body.
  • File import — Canvas Files in an upload-accepted format under the shared 50 MB limit, downloaded with maxBytes, written under uploads/ like a multer upload, handed to the existing createMaterials → ingestMaterial pipeline. Material.origin + the fixed-name partial unique index materials_origin_unique make a re-import a skip, never a duplicate. Per-file independent.
  • Roster sync — matched on Canvas integration_id (= PUID at UBC) only, no fallback key. Stored in a new lmsRosterEntries collection with two unique indexes; each sync replaces the course's set. A roster exposing no integration_id is refused (409 roster-coverage) and nothing is written.
  • Enrollment gate — passes on the CSV roster or a Canvas entry by PUID. The Canvas lookup runs only when the CSV roster did not match, so existing behaviour is byte-identical. Synced entries add to the CSV roster; the CSV path is the escape hatch (e.g. a student whose Canvas account has no integration_id).
  • UI — Settings gains a Canvas card (connect → choose course → linked; sync report with coverage; unlink; disconnect). Materials gains Import from Canvas for linked courses.
  • PRD — Canvas linking/import/roster sync moved into scope; gradebook write-back stays a stretch goal.

Not in it, on purpose

No writes to Canvas (grades, feedback). No Moodle. No submission import.

Verification

  • Full suite green: 102 suites / 1,291 tests (+45 new). Typecheck, lint, client build clean.
  • End-to-end smoke against local Canvas (../local-lms-dev/, verified 2026-08-27) as a plain non-admin teacher — every step matched the prediction, with each stage confirmed in the database. Table in docs/superpowers/plans/phase-6/Saurav/STATUS.md. Highlights: a student with no CSV entry enrolled via the Canvas roster alone; a Canvas user without integration_id was reported and not stored; unlink left enrollments and imported materials intact.

Reviewer notes

  • materials.routes.test.ts mocks the whole materials.service; the upload limits now come from that module, so the mock supplies them. Anyone else mocking it wholesale will need the same.
  • The package's structural MongoDbLike type doesn't satisfy mongodb v7's Db at the type level (createIndex param variance); one cast at the boundary in components/lms/index.ts, runtime-compatible. Worth an upstream note.
  • Before go-live: the production Developer Key must be scoped to the five endpoints in the spec's Configuration section with Allow Include Parameters on, and someone should confirm with LT Hub that hosted UBC Canvas populates integration_id with the PUID and exposes it to Teacher-role tokens. Locally it does; hosted is a separate account-level configuration.

sroygit added 10 commits August 27, 2026 10:28
Canvas LMS integration package from GitHub Packages. The project .npmrc
carries only the @ubc registry line; the read:packages PAT lives in each
developer's ~/.npmrc and is never committed.
…, Saurav's plan

Read-only Canvas integration: connect via OAuth, link a Canvas course the
instructor teaches, import Canvas Files into materials, sync the roster
matched on integration_id (= PUID). Synced entries add to the CSV roster.
No gradebook write-back, no Moodle.
Binds @ubc/ubc-genai-toolkit-lms-integration to FinanceBot identity
(getUserKey = req.user.puid) and a Mongo token store on lmsCanvasTokens.
Mounts the package OAuth router and GET /api/lms/canvas/status under /api
only when all four CANVAS_* variables are set. Verified by hand against
local Canvas: connect stores one token keyed by PUID, disconnect removes it.
GET /courses lists only courses the connected Canvas identity teaches;
PUT link refuses any id outside that list (403 not-teacher) and stores
name/code from Canvas's own row. DELETE link also clears the course's
synced roster entries. Adds Course.canvas, the LmsRosterEntry type and
its collection accessor.
GET files lists Canvas Files in an upload-accepted format under the shared
size limit, flagging ones already imported. POST files/import downloads
each with maxBytes, writes it under UPLOAD_DIR like a multer upload, and
hands it to createMaterials; already-imported ids are skipped, one failure
does not stop the rest. Material.origin plus the fixed-name partial unique
index make a re-import a skip, never a duplicate. The upload batch/size
constants move to materials.service so the policy has one definition.
POST roster/sync reads the linked course's Canvas roster, builds a match
report against students enrolled in this course (keyed by PUID), explains
the unmatched, and replaces the course's lmsRosterEntries with every Canvas
user carrying an integration_id. A roster exposing no integration_id at all
is refused (409 roster-coverage) and nothing is written. Two unique indexes
make a duplicate Canvas identity per PUID a loud failure.
The registration-code gate now passes when the student is on the CSV
roster OR on the course's synced Canvas roster (lmsRosterEntries, keyed by
PUID). The Canvas lookup runs only when the CSV roster did not match, so
existing behaviour is byte-identical. PRD updated: Canvas linking, file
import and roster sync are in scope; gradebook write-back stays a stretch
goal.
…aterials

Settings gains a Canvas card — connect, choose the Canvas course you teach,
linked — with Sync roster, Unlink, and Disconnect, and a sync report that
shows matched / on-Canvas-only / in-FinanceBot-only counts next to the
student-ID coverage that makes them trustworthy. The roster heading reads
'N from CSV · M from Canvas'. Materials gains an Import-from-Canvas dialog,
shown only for a linked course, that disables already-imported files and
reads the selection back before importing.
npm 11 on macOS prunes the optional @emnapi/* entries that npm 10 on the
runner still expects, so npm ci failed with 'Missing: @emnapi/core'. The
lock is now origin/main's plus only the lms-integration entry. CI also
needs a token for the @ubc scope: setup-node writes the registry auth
line from NODE_AUTH_TOKEN, which prefers a GH_PACKAGES_READ_TOKEN secret
and falls back to GITHUB_TOKEN.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant