mirror of
https://github.com/alexmickelson/canvasManagement.git
synced 2026-03-25 23:28:33 -06:00
workign on canvas api requests
This commit is contained in:
@@ -1,19 +1,7 @@
|
|||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
|
|
||||||
const token = process.env.NEXT_PUBLIC_CANVAS_TOKEN;
|
|
||||||
if (!token) {
|
|
||||||
throw new Error("CANVAS_TOKEN not in environment");
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
async rewrites() {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
source: "/api/canvas/:rest*",
|
|
||||||
destination: "https://snow.instructure.com/api/v1/:rest*",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
28
nextjs/src/app/api/canvas/[...rest]/route.ts
Normal file
28
nextjs/src/app/api/canvas/[...rest]/route.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { axiosClient } from "@/services/axiosUtils";
|
||||||
|
|
||||||
|
export async function GET(
|
||||||
|
req: NextRequest,
|
||||||
|
{ params }: { params: { rest: string[] } }
|
||||||
|
) {
|
||||||
|
const { rest } = params;
|
||||||
|
const path = rest.join("/");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newUrl = `https://snow.instructure.com/api/v1/${path}`;
|
||||||
|
const response = await axiosClient.get(newUrl, {
|
||||||
|
headers: {
|
||||||
|
// Include other headers from the incoming request if needed:
|
||||||
|
// 'Content-Type': req.headers.get('content-type') || 'application/json',
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json(response.data);
|
||||||
|
} catch (error: any) {
|
||||||
|
return new NextResponse(
|
||||||
|
JSON.stringify({ error: error.message || "Request failed" }),
|
||||||
|
{ status: error.response?.status || 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,38 +2,30 @@ import { isServer } from "@tanstack/react-query";
|
|||||||
import axios, { AxiosInstance, AxiosError, AxiosHeaders } from "axios";
|
import axios, { AxiosInstance, AxiosError, AxiosHeaders } from "axios";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
|
|
||||||
const token = process.env.NEXT_PUBLIC_CANVAS_TOKEN;
|
const canvasBaseUrl = "https://snow.instructure.com/api/v1/";
|
||||||
if (!token) {
|
|
||||||
throw new Error("NEXT_PUBLIC_CANVAS_TOKEN not in environment");
|
|
||||||
}
|
|
||||||
|
|
||||||
export const axiosClient: AxiosInstance = axios.create();
|
export const axiosClient: AxiosInstance = axios.create();
|
||||||
|
|
||||||
if (!isServer) {
|
if (!isServer) {
|
||||||
axiosClient.interceptors.request.use((config) => {
|
axiosClient.interceptors.request.use((config) => {
|
||||||
if (
|
if (config.url && config.url.startsWith(canvasBaseUrl)) {
|
||||||
config.url &&
|
const newUrl = config.url.replace(canvasBaseUrl, "/api/canvas/");
|
||||||
config.url.startsWith("https://snow.instructure.com/api/v1/")
|
|
||||||
) {
|
|
||||||
const newUrl = config.url.replace(
|
|
||||||
"https://snow.instructure.com/api/v1/",
|
|
||||||
"/api/canvas/"
|
|
||||||
);
|
|
||||||
config.url = newUrl;
|
config.url = newUrl;
|
||||||
}
|
}
|
||||||
return config;
|
return config;
|
||||||
});
|
});
|
||||||
}
|
} else {
|
||||||
|
const token = process.env.CANVAS_TOKEN;
|
||||||
axiosClient.interceptors.request.use((config) => {
|
if (!token) {
|
||||||
if (
|
throw new Error("CANVAS_TOKEN not in environment");
|
||||||
config.url &&
|
|
||||||
config.url.startsWith("https://snow.instructure.com/api/v1/")
|
|
||||||
) {
|
|
||||||
config.headers.set("Authorization", `Bearer ${token}`);
|
|
||||||
}
|
}
|
||||||
return config;
|
axiosClient.interceptors.request.use((config) => {
|
||||||
});
|
if (config.url && config.url.startsWith(canvasBaseUrl)) {
|
||||||
|
config.headers.set("Authorization", `Bearer ${token}`);
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
axiosClient.interceptors.response.use(
|
axiosClient.interceptors.response.use(
|
||||||
(response) => response,
|
(response) => response,
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { CanvasPage } from "@/models/canvas/pages/canvasPageModel";
|
import { CanvasPage } from "@/models/canvas/pages/canvasPageModel";
|
||||||
import { LocalCoursePage } from "@/models/local/page/localCoursePage";
|
import { LocalCoursePage } from "@/models/local/page/localCoursePage";
|
||||||
import { canvasServiceUtils } from "./canvasServiceUtils";
|
import { canvasServiceUtils } from "./canvasServiceUtils";
|
||||||
import { webRequestor } from "./webRequestor";
|
|
||||||
|
|
||||||
const baseCanvasUrl = "https://snow.instructure.com/api/v1";
|
const baseCanvasUrl = "https://snow.instructure.com/api/v1";
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { CanvasEnrollmentTermModel } from "@/models/canvas/enrollmentTerms/canvasEnrollmentTermModel";
|
import { CanvasEnrollmentTermModel } from "@/models/canvas/enrollmentTerms/canvasEnrollmentTermModel";
|
||||||
import { canvasServiceUtils } from "./canvasServiceUtils";
|
import { canvasServiceUtils } from "./canvasServiceUtils";
|
||||||
import { webRequestor } from "./webRequestor";
|
|
||||||
import { CanvasCourseModel } from "@/models/canvas/courses/canvasCourseModel";
|
import { CanvasCourseModel } from "@/models/canvas/courses/canvasCourseModel";
|
||||||
import { CanvasModuleItem } from "@/models/canvas/modules/canvasModuleItems";
|
import { CanvasModuleItem } from "@/models/canvas/modules/canvasModuleItems";
|
||||||
import { CanvasPage } from "@/models/canvas/pages/canvasPageModel";
|
import { CanvasPage } from "@/models/canvas/pages/canvasPageModel";
|
||||||
import { CanvasEnrollmentModel } from "@/models/canvas/enrollments/canvasEnrollmentModel";
|
import { CanvasEnrollmentModel } from "@/models/canvas/enrollments/canvasEnrollmentModel";
|
||||||
|
import { axiosClient } from "../axiosUtils";
|
||||||
|
|
||||||
const getTerms = async () => {
|
const getTerms = async () => {
|
||||||
const url = `accounts/10/terms`;
|
const url = `accounts/10/terms`;
|
||||||
@@ -28,7 +28,7 @@ export const canvasService = {
|
|||||||
|
|
||||||
async getCourse(courseId: number): Promise<CanvasCourseModel> {
|
async getCourse(courseId: number): Promise<CanvasCourseModel> {
|
||||||
const url = `courses/${courseId}`;
|
const url = `courses/${courseId}`;
|
||||||
const { data, response } = await webRequestor.get<CanvasCourseModel>(url);
|
const { data } = await axiosClient.get<CanvasCourseModel>(url);
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -62,8 +62,7 @@ export const canvasService = {
|
|||||||
const body = {
|
const body = {
|
||||||
module_item: { title: item.title, position: item.position },
|
module_item: { title: item.title, position: item.position },
|
||||||
};
|
};
|
||||||
const { data, response } =
|
const { data } = await axiosClient.put<CanvasModuleItem>(url, body);
|
||||||
await webRequestor.putWithDeserialize<CanvasModuleItem>(url, body);
|
|
||||||
|
|
||||||
if (!data) throw new Error("Something went wrong updating module item");
|
if (!data) throw new Error("Something went wrong updating module item");
|
||||||
},
|
},
|
||||||
@@ -78,10 +77,7 @@ export const canvasService = {
|
|||||||
console.log(`Creating new module item ${title}`);
|
console.log(`Creating new module item ${title}`);
|
||||||
const url = `courses/${canvasCourseId}/modules/${canvasModuleId}/items`;
|
const url = `courses/${canvasCourseId}/modules/${canvasModuleId}/items`;
|
||||||
const body = { module_item: { title, type, content_id: contentId } };
|
const body = { module_item: { title, type, content_id: contentId } };
|
||||||
const response = await webRequestor.post(url, body);
|
const response = await axiosClient.post(url, body);
|
||||||
|
|
||||||
if (!response.ok)
|
|
||||||
throw new Error("Something went wrong creating module item");
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async createPageModuleItem(
|
async createPageModuleItem(
|
||||||
@@ -95,18 +91,13 @@ export const canvasService = {
|
|||||||
const body = {
|
const body = {
|
||||||
module_item: { title, type: "Page", page_url: canvasPage.url },
|
module_item: { title, type: "Page", page_url: canvasPage.url },
|
||||||
};
|
};
|
||||||
const { data, response } =
|
await axiosClient.post<CanvasModuleItem>(url, body);
|
||||||
await webRequestor.postWithDeserialize<CanvasModuleItem>(url, body);
|
|
||||||
|
|
||||||
if (!response.ok)
|
|
||||||
throw new Error("Something went wrong creating page module item");
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async getEnrolledStudents(canvasCourseId: number) {
|
async getEnrolledStudents(canvasCourseId: number) {
|
||||||
console.log(`Getting students for course ${canvasCourseId}`);
|
console.log(`Getting students for course ${canvasCourseId}`);
|
||||||
const url = `courses/${canvasCourseId}/enrollments?enrollment_type=student`;
|
const url = `courses/${canvasCourseId}/enrollments?enrollment_type=student`;
|
||||||
const { data, response } =
|
const { data } = await axiosClient.get<CanvasEnrollmentModel[]>(url);
|
||||||
await webRequestor.getMany<CanvasEnrollmentModel>(url);
|
|
||||||
|
|
||||||
if (!data)
|
if (!data)
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// services/canvasServiceUtils.ts
|
// services/canvasServiceUtils.ts
|
||||||
|
|
||||||
import { AxiosResponseHeaders, RawAxiosResponseHeaders } from "axios";
|
import { AxiosResponseHeaders, RawAxiosResponseHeaders } from "axios";
|
||||||
import { webRequestor } from "./webRequestor";
|
import { axiosClient } from "../axiosUtils";
|
||||||
|
|
||||||
const getNextUrl = (
|
const getNextUrl = (
|
||||||
headers: AxiosResponseHeaders | RawAxiosResponseHeaders
|
headers: AxiosResponseHeaders | RawAxiosResponseHeaders
|
||||||
@@ -28,19 +28,20 @@ export const canvasServiceUtils = {
|
|||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
url.searchParams.set("per_page", "100");
|
url.searchParams.set("per_page", "100");
|
||||||
|
|
||||||
const { data: firstData, response: firstResponse } =
|
const { data: firstData, headers: firstHeaders } = await axiosClient.get<T>(
|
||||||
await webRequestor.get<T>(url.toString());
|
url.toString()
|
||||||
|
);
|
||||||
|
|
||||||
var returnData: T[] = firstData ? [firstData] : [];
|
var returnData: T[] = firstData ? [firstData] : [];
|
||||||
var nextUrl = getNextUrl(firstResponse.headers);
|
var nextUrl = getNextUrl(firstHeaders);
|
||||||
|
|
||||||
while (nextUrl) {
|
while (nextUrl) {
|
||||||
requestCount += 1;
|
requestCount += 1;
|
||||||
const { data, response } = await webRequestor.get<T>(nextUrl);
|
const { data, headers } = await axiosClient.get<T>(nextUrl);
|
||||||
if (data) {
|
if (data) {
|
||||||
returnData = [...returnData, data];
|
returnData = [...returnData, data];
|
||||||
}
|
}
|
||||||
nextUrl = getNextUrl(response.headers);
|
nextUrl = getNextUrl(headers);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (requestCount > 1) {
|
if (requestCount > 1) {
|
||||||
|
|||||||
66
nextjs/src/services/canvas/canvasWebRequestor.ts
Normal file
66
nextjs/src/services/canvas/canvasWebRequestor.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { AxiosResponse } from "axios";
|
||||||
|
import { axiosClient } from "../axiosUtils";
|
||||||
|
|
||||||
|
type FetchOptions = Omit<RequestInit, "method">;
|
||||||
|
|
||||||
|
const rateLimitRetryCount = 6;
|
||||||
|
const rateLimitSleepInterval = 1000;
|
||||||
|
|
||||||
|
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
|
||||||
|
export const isRateLimited = async (
|
||||||
|
response: AxiosResponse
|
||||||
|
): Promise<boolean> => {
|
||||||
|
const content = await response.data;
|
||||||
|
return (
|
||||||
|
response.status === 403 &&
|
||||||
|
content.includes("403 Forbidden (Rate Limit Exceeded)")
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const rateLimitAwarePost = async (url: string, body: any, retryCount = 0) => {
|
||||||
|
const response = await axiosClient.post(url, body);
|
||||||
|
|
||||||
|
if (await isRateLimited(response)) {
|
||||||
|
if (retryCount < rateLimitRetryCount) {
|
||||||
|
console.info(
|
||||||
|
`Hit rate limit on post, retry count is ${retryCount} / ${rateLimitRetryCount}, retrying`
|
||||||
|
);
|
||||||
|
await sleep(rateLimitSleepInterval);
|
||||||
|
return await rateLimitAwarePost(url, body, retryCount + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const rateLimitAwareDelete = async (
|
||||||
|
url: string,
|
||||||
|
retryCount = 0
|
||||||
|
): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const response = await axiosClient.delete(url);
|
||||||
|
|
||||||
|
if (await isRateLimited(response)) {
|
||||||
|
console.info("After delete response in rate limited");
|
||||||
|
await sleep(rateLimitSleepInterval);
|
||||||
|
return await rateLimitAwareDelete(url, retryCount + 1);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
const error = e as Error & { response?: Response };
|
||||||
|
if (error.response?.status === 403) {
|
||||||
|
if (retryCount < rateLimitRetryCount) {
|
||||||
|
console.info(
|
||||||
|
`Hit rate limit in delete, retry count is ${retryCount} / ${rateLimitRetryCount}, retrying`
|
||||||
|
);
|
||||||
|
await sleep(rateLimitSleepInterval);
|
||||||
|
return await rateLimitAwareDelete(url, retryCount + 1);
|
||||||
|
} else {
|
||||||
|
console.info(
|
||||||
|
`Hit rate limit in delete, ${rateLimitRetryCount} retries did not fix it`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,154 +0,0 @@
|
|||||||
import { axiosClient } from "../axiosUtils";
|
|
||||||
|
|
||||||
type FetchOptions = Omit<RequestInit, "method">;
|
|
||||||
|
|
||||||
const rateLimitRetryCount = 6;
|
|
||||||
const rateLimitSleepInterval = 1000;
|
|
||||||
|
|
||||||
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
||||||
|
|
||||||
const isRateLimited = async (response: Response): Promise<boolean> => {
|
|
||||||
const content = await response.text();
|
|
||||||
return (
|
|
||||||
response.status === 403 &&
|
|
||||||
content.includes("403 Forbidden (Rate Limit Exceeded)")
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const deserialize = async <T>(response: Response): Promise<T | undefined> => {
|
|
||||||
if (!response.ok) {
|
|
||||||
console.error(`Error with response to ${response.url} ${response.status}`);
|
|
||||||
throw new Error(
|
|
||||||
`Error with response to ${response.url} ${response.status}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return (await response.json()) as T;
|
|
||||||
} catch (e) {
|
|
||||||
console.error(`An error occurred during deserialization: ${e}`);
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const rateLimitAwarePost = async (
|
|
||||||
url: string,
|
|
||||||
body: any,
|
|
||||||
retryCount = 0
|
|
||||||
): Promise<Response> => {
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (await isRateLimited(response)) {
|
|
||||||
if (retryCount < rateLimitRetryCount) {
|
|
||||||
console.info(
|
|
||||||
`Hit rate limit on post, retry count is ${retryCount} / ${rateLimitRetryCount}, retrying`
|
|
||||||
);
|
|
||||||
await sleep(rateLimitSleepInterval);
|
|
||||||
return await rateLimitAwarePost(url, body, retryCount + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const content = await response.text();
|
|
||||||
console.error(`Error with response, response content: ${content}`);
|
|
||||||
throw new Error(
|
|
||||||
`Error post response, retrycount: ${retryCount}, ratelimited: ${await isRateLimited(
|
|
||||||
response
|
|
||||||
)}, code: ${response.status}, response content: ${content}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return response;
|
|
||||||
};
|
|
||||||
|
|
||||||
const recursiveDelete = async (
|
|
||||||
url: string,
|
|
||||||
options: FetchOptions,
|
|
||||||
retryCount = 0
|
|
||||||
): Promise<Response> => {
|
|
||||||
try {
|
|
||||||
const response = await fetch(url, {
|
|
||||||
...options,
|
|
||||||
method: "DELETE",
|
|
||||||
headers: {
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (await isRateLimited(response)) {
|
|
||||||
console.info("After delete response in rate limited");
|
|
||||||
await sleep(rateLimitSleepInterval);
|
|
||||||
return await recursiveDelete(url, options, retryCount + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
|
||||||
} catch (e) {
|
|
||||||
const error = e as Error & { response?: Response };
|
|
||||||
if (error.response?.status === 403) {
|
|
||||||
if (retryCount < rateLimitRetryCount) {
|
|
||||||
console.info(
|
|
||||||
`Hit rate limit in delete, retry count is ${retryCount} / ${rateLimitRetryCount}, retrying`
|
|
||||||
);
|
|
||||||
await sleep(rateLimitSleepInterval);
|
|
||||||
return await recursiveDelete(url, options, retryCount + 1);
|
|
||||||
} else {
|
|
||||||
console.info(
|
|
||||||
`Hit rate limit in delete, ${rateLimitRetryCount} retries did not fix it`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
export const webRequestor = {
|
|
||||||
getMany: async <T>(url: string, options: FetchOptions = {}) => {
|
|
||||||
const response = await axiosClient.get<T[]>(url);
|
|
||||||
return { data: response.data, response };
|
|
||||||
},
|
|
||||||
|
|
||||||
get: async <T>(url: string) => {
|
|
||||||
const response = await axiosClient.get<T>(url);
|
|
||||||
return { data: response.data, response };
|
|
||||||
},
|
|
||||||
|
|
||||||
post: async (url: string, body: any) => {
|
|
||||||
return await rateLimitAwarePost(url, body);
|
|
||||||
},
|
|
||||||
|
|
||||||
postWithDeserialize: async <T>(url: string, body: any) => {
|
|
||||||
const response = await rateLimitAwarePost(url, body);
|
|
||||||
return { data: await deserialize<T[]>(response), response };
|
|
||||||
},
|
|
||||||
|
|
||||||
put: async (url: string, body: any = {}) => {
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method: "PUT",
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return response;
|
|
||||||
},
|
|
||||||
|
|
||||||
putWithDeserialize: async <T>(url: string, body: any = {}) => {
|
|
||||||
const response = await fetch(url, {
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
method: "PUT",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return { data: await deserialize<T[]>(response), response };
|
|
||||||
},
|
|
||||||
|
|
||||||
delete: async (
|
|
||||||
url: string,
|
|
||||||
options: FetchOptions = {}
|
|
||||||
): Promise<Response> => {
|
|
||||||
return await recursiveDelete(url, options);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user