feat: enhance API and middleware functionality: add public API routes, implement program and course member checks, and improve rate limiting for invite acceptance. Update OpenAPI spec generation to focus on public API endpoints and add new middleware for automation key scopes. Include new documentation for lesson AI features and public API tests.

This commit is contained in:
Rotimi Best
2026-04-12 08:58:41 +01:00
parent 5a8f50f67b
commit d6e812d08b
573 changed files with 31964 additions and 8708 deletions
@@ -1,9 +1,12 @@
import { BaseApiWithErrors, classroomio } from '$lib/utils/services/api';
import type {
GetOrgCoursesRequest,
GetOrgCoursesRequestQuery,
GetRecommendedCoursesRequest,
GetUserEnrolledCoursesRequest,
OrgCourses,
OrgCoursesPagination,
OrgCoursesQuery,
RecommendedCourses,
UserEnrolledCourses
} from '$features/course/types';
@@ -15,8 +18,15 @@ import { SvelteSet } from 'svelte/reactivity';
*/
export class CoursesApi extends BaseApiWithErrors {
orgCourses = $state<OrgCourses>([]);
orgCoursesPagination = $state<OrgCoursesPagination | null>(null);
enrolledCourses = $state<UserEnrolledCourses>([]);
recommendedCourses = $state<RecommendedCourses>([]);
private activeOrgCoursesRequestController: AbortController | null = null;
cancelOrgCoursesRequest() {
this.activeOrgCoursesRequestController?.abort();
this.activeOrgCoursesRequestController = null;
}
/** Keeps list UIs in sync after a course is deleted (server data is updated separately on navigation). */
removeCourseFromLists(courseId: string) {
@@ -30,26 +40,79 @@ export class CoursesApi extends BaseApiWithErrors {
* Org ID is automatically added from currentOrg store
*/
async getOrgCourses(tagSlugs: string[] = []) {
const normalizedTagSlugs = Array.from(new SvelteSet(tagSlugs.map((tag) => tag.trim()).filter(Boolean)));
const allCourses: OrgCourses = [];
let page = 1;
let totalPages = 1;
let lastResponse: Awaited<ReturnType<CoursesApi['getOrgCoursesPage']>> | undefined;
return this.execute<GetOrgCoursesRequest>({
while (page <= totalPages) {
const response = await this.getOrgCoursesPage({ page, limit: 100 }, tagSlugs);
if (!response) {
return response;
}
allCourses.push(...(response.data ?? []));
totalPages = response.pagination?.totalPages ?? 1;
lastResponse = response;
page += 1;
}
this.orgCourses = allCourses;
this.orgCoursesPagination = lastResponse?.pagination ?? null;
return lastResponse;
}
async getOrgCoursesPage(
query: Partial<OrgCoursesQuery> = {},
tagSlugs: string[] = [],
options: { abortPrevious?: boolean; signal?: AbortSignal } = {}
) {
const normalizedTagSlugs = Array.from(new SvelteSet(tagSlugs.map((tag) => tag.trim()).filter(Boolean)));
const requestQuery: GetOrgCoursesRequestQuery = {
page: String(query.page ?? 1),
limit: String(query.limit ?? 20),
search: query.search,
tags: normalizedTagSlugs.length > 0 ? normalizedTagSlugs.join(',') : undefined
};
let requestSignal = options.signal;
let requestController: AbortController | null = null;
if (options.abortPrevious) {
this.cancelOrgCoursesRequest();
requestController = new AbortController();
this.activeOrgCoursesRequestController = requestController;
requestSignal = requestController.signal;
}
const response = await this.execute<GetOrgCoursesRequest>({
requestFn: () =>
classroomio.organization.courses.$get(
normalizedTagSlugs.length > 0
? {
query: {
tags: normalizedTagSlugs.join(',')
}
}
: { query: {} }
{
query: requestQuery
},
{
init: {
signal: requestSignal
}
}
),
logContext: 'fetching org courses',
onSuccess: (response) => {
if (response.data) {
this.orgCourses = response.data;
}
this.orgCoursesPagination = response.pagination;
}
});
if (requestController && this.activeOrgCoursesRequestController === requestController) {
this.activeOrgCoursesRequestController = null;
}
return response;
}
/**