Brings the share build up to date with the main project: QTI .zip export and optional direct Canvas API push, plus the Canvas settings card. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
91 lines
3.6 KiB
JavaScript
91 lines
3.6 KiB
JavaScript
import { NextResponse } from "next/server";
|
|
import { mergeSettings, getSettings } from "@/lib/store";
|
|
import { mapAssignmentToCanvas } from "@/lib/canvas/map";
|
|
import { normalizeBaseUrl, quizPayload, questionPayloads } from "@/lib/canvas/api";
|
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
// Browser→Canvas is blocked by CORS, so all Canvas REST calls go through this server route.
|
|
// Token + base URL come from the saved local settings (data/db.json), like the AI keys.
|
|
function creds(settings) {
|
|
const c = settings?.canvas || {};
|
|
const baseUrl = normalizeBaseUrl(c.baseUrl);
|
|
const token = String(c.token || "").trim();
|
|
if (!baseUrl) throw new Error("Add your Canvas web address in Settings first.");
|
|
if (!token) throw new Error("Add your Canvas access token in Settings first.");
|
|
return { baseUrl, token };
|
|
}
|
|
|
|
async function canvas(baseUrl, token, path, init = {}) {
|
|
let res;
|
|
try {
|
|
res = await fetch(baseUrl + path, {
|
|
...init,
|
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", ...(init.headers || {}) },
|
|
});
|
|
} catch {
|
|
throw new Error("Couldn't reach Canvas — check the web address and your network.");
|
|
}
|
|
const text = await res.text();
|
|
let data;
|
|
try { data = text ? JSON.parse(text) : {}; } catch { data = { raw: text }; }
|
|
if (!res.ok) {
|
|
if (res.status === 401) throw new Error("Canvas rejected the access token (401). Check or regenerate it.");
|
|
const msg = data?.errors?.[0]?.message || data?.message ||
|
|
(Array.isArray(data?.errors) ? JSON.stringify(data.errors) : "") || `Canvas returned ${res.status}.`;
|
|
throw new Error(typeof msg === "string" ? msg : `Canvas error ${res.status}.`);
|
|
}
|
|
return data;
|
|
}
|
|
|
|
export async function POST(request) {
|
|
try {
|
|
const body = await request.json();
|
|
// Accept settings from the client (test-before-save) or fall back to what's saved.
|
|
const settings = mergeSettings(body.settings || getSettings());
|
|
const { baseUrl, token } = creds(settings);
|
|
const action = body.action;
|
|
|
|
if (action === "test") {
|
|
const me = await canvas(baseUrl, token, "/api/v1/users/self");
|
|
return NextResponse.json({ ok: true, message: `Connected as ${me.name || me.short_name || "Canvas user"}` });
|
|
}
|
|
|
|
if (action === "courses") {
|
|
const list = await canvas(baseUrl, token,
|
|
"/api/v1/courses?enrollment_type=teacher&enrollment_state=active&per_page=100");
|
|
const courses = (Array.isArray(list) ? list : [])
|
|
.filter((c) => c?.id && c?.name && !c.access_restricted_by_date)
|
|
.map((c) => ({ id: c.id, name: c.name }));
|
|
return NextResponse.json({ courses });
|
|
}
|
|
|
|
if (action === "push") {
|
|
const { assignment, opts, courseId } = body;
|
|
if (!courseId) throw new Error("Pick a course first.");
|
|
const ir = mapAssignmentToCanvas(assignment, opts || {});
|
|
if (ir.gradedCount === 0) throw new Error("This assignment has no Canvas-compatible questions.");
|
|
|
|
const quiz = await canvas(baseUrl, token, `/api/v1/courses/${courseId}/quizzes`, {
|
|
method: "POST", body: JSON.stringify(quizPayload(ir)),
|
|
});
|
|
const quizId = quiz.id;
|
|
|
|
let added = 0;
|
|
for (const q of questionPayloads(ir)) {
|
|
await canvas(baseUrl, token, `/api/v1/courses/${courseId}/quizzes/${quizId}/questions`, {
|
|
method: "POST", body: JSON.stringify(q),
|
|
});
|
|
added++;
|
|
}
|
|
|
|
const url = quiz.html_url || `${baseUrl}/courses/${courseId}/quizzes/${quizId}`;
|
|
return NextResponse.json({ ok: true, url, quizId, added });
|
|
}
|
|
|
|
throw new Error("Unknown action.");
|
|
} catch (e) {
|
|
return NextResponse.json({ error: String(e.message || e) }, { status: 400 });
|
|
}
|
|
}
|