480 lines
14 KiB
Markdown
480 lines
14 KiB
Markdown
# Course Management API Integration Guide
|
|
|
|
This document describes the Course Management related APIs used by the admin frontend (`Yimaru-Admin`) and how to integrate them safely.
|
|
|
|
It is based on:
|
|
- `src/api/courses.api.ts`
|
|
- `src/api/files.api.ts`
|
|
- `src/types/course.types.ts`
|
|
- `src/api/http.ts`
|
|
|
|
---
|
|
|
|
## 1) Base setup and auth behavior
|
|
|
|
### Base URL
|
|
- All requests use `VITE_API_BASE_URL` from environment.
|
|
|
|
### Authentication
|
|
- Access token is sent automatically as `Authorization: Bearer <access_token>`.
|
|
- On `401`, the frontend attempts token refresh via:
|
|
- `POST /auth/refresh`
|
|
- payload: `{ access_token, refresh_token, role, member_id }`
|
|
- If refresh fails, auth data is cleared and user is redirected to `/login`.
|
|
|
|
### Transport notes
|
|
- Axios automatically handles `multipart/form-data` boundaries for file upload.
|
|
- Any network failure without response also redirects to `/login` (current client behavior).
|
|
|
|
---
|
|
|
|
## 2) Core domain model used by frontend
|
|
|
|
Current hierarchy used by content management:
|
|
- `Category`
|
|
- `Sub-category`
|
|
- `Course`
|
|
- `Level (CEFR)`
|
|
- `Module`
|
|
- `Sub-module`
|
|
- `Videos`
|
|
- `Lessons` (question sets with `set_type = QUIZ`)
|
|
- `Practices` (question sets with `set_type = PRACTICE`)
|
|
|
|
Important migration note:
|
|
- Some APIs/types are marked as legacy (`Program`, old `Level/Module` flows).
|
|
- Current frontend mostly uses unified hierarchy endpoints under `/course-management/...` plus `/question-sets` and `/questions`.
|
|
|
|
---
|
|
|
|
## 3) File/media APIs (used by course management)
|
|
|
|
## 3.1 Upload media
|
|
|
|
### Endpoint
|
|
- `POST /files/upload`
|
|
|
|
### Supports
|
|
- `media_type`: `"image" | "audio" | "video"`
|
|
- File upload via multipart (`file`) or URL import via JSON (`source_url`).
|
|
|
|
### For video uploads
|
|
- Can send optional `title` and `description`.
|
|
|
|
### Typical response fields used by frontend
|
|
- `data.object_key`
|
|
- `data.url`
|
|
- `data.provider` (`MINIO` or `VIMEO`)
|
|
- `data.vimeo_id`
|
|
- `data.embed_url`
|
|
|
|
### Frontend wrapper functions
|
|
- `uploadAudioFile(fileOrUrl)`
|
|
- `uploadImageFile(fileOrUrl)`
|
|
- `uploadVideoFile(fileOrUrl, { title?, description? })`
|
|
|
|
## 3.2 Resolve object key to URL
|
|
|
|
### Endpoint
|
|
- `GET /files/url?key=<object_key>`
|
|
|
|
### Use case
|
|
- Resolve media object key when only key is stored.
|
|
|
|
---
|
|
|
|
## 4) Category and course APIs
|
|
|
|
## 4.1 Get categories (normalized in frontend)
|
|
|
|
### Endpoint called
|
|
- `GET /course-management/hierarchy`
|
|
|
|
### Frontend behavior
|
|
- Client transforms flat hierarchy rows into category list.
|
|
- Duplicated category names are merged client-side by "richest" record.
|
|
|
|
### Wrapper
|
|
- `getCourseCategories()`
|
|
|
|
## 4.2 Create category or sub-category
|
|
|
|
### Category
|
|
- `POST /course-management/categories`
|
|
- body: `{ name }`
|
|
|
|
### Sub-category
|
|
- `POST /course-management/sub-categories`
|
|
- body: `{ category_id, name }`
|
|
|
|
### Wrapper
|
|
- `createCourseCategory({ name, parent_id? })`
|
|
- if `parent_id` exists, creates sub-category; else category.
|
|
|
|
## 4.3 Delete category/sub-category
|
|
- `DELETE /course-management/categories/:categoryId`
|
|
- `DELETE /course-management/sub-categories/:subCategoryId`
|
|
|
|
Wrappers:
|
|
- `deleteCourseCategory(categoryId)`
|
|
- `deleteCourseSubCategory(subCategoryId)`
|
|
|
|
## 4.4 Courses by category
|
|
|
|
### Endpoint called
|
|
- `GET /course-management/hierarchy`
|
|
|
|
### Frontend behavior
|
|
- Filters and maps rows to courses client-side.
|
|
- If duplicate category names exist, it includes rows matching requested category name.
|
|
|
|
Wrapper:
|
|
- `getCoursesByCategory(categoryId)`
|
|
|
|
## 4.5 Course CRUD
|
|
- `POST /course-management/courses`
|
|
- `PUT /course-management/courses/:courseId`
|
|
- `PUT /course-management/courses/:courseId` (status toggle via `is_active`)
|
|
- `DELETE /course-management/courses/:courseId`
|
|
- `POST /course-management/courses/:courseId/thumbnail`
|
|
|
|
Wrappers:
|
|
- `createCourse(data)`
|
|
- `updateCourse(courseId, data)`
|
|
- `updateCourseStatus(courseId, isActive)`
|
|
- `deleteCourse(courseId)`
|
|
- `updateCourseThumbnail(courseId, thumbnailUrl)`
|
|
|
|
---
|
|
|
|
## 5) Course hierarchy (levels/modules/sub-modules)
|
|
|
|
## 5.1 Get full hierarchy for one course
|
|
|
|
### Endpoint
|
|
- `GET /course-management/courses/:courseId/hierarchy`
|
|
|
|
### Wrapper
|
|
- `getSubModulesByCourse(courseId)`
|
|
|
|
### Frontend behavior
|
|
- Maps hierarchy rows into `sub_courses` shape (compatibility naming).
|
|
- This is the primary source for module/sub-module tree rendering.
|
|
|
|
## 5.2 Create sub-module flow (composed)
|
|
|
|
`createSubModule(data)` is a multi-step client workflow:
|
|
1. `POST /course-management/levels`
|
|
2. `POST /course-management/modules`
|
|
3. `POST /course-management/sub-modules`
|
|
|
|
Use this when creating a new sub-module from minimal info.
|
|
|
|
## 5.3 Direct level/module/sub-module creation
|
|
- `createModuleInLevel(levelId, title, description, displayOrder?)`
|
|
- `POST /course-management/modules`
|
|
- `createSubModuleInModule(moduleId, title, description, displayOrder?)`
|
|
- `POST /course-management/sub-modules`
|
|
|
|
## 5.4 Update/delete sub-module
|
|
- `PUT /course-management/sub-modules/:subModuleId`
|
|
- `PUT /course-management/sub-modules/:subModuleId` (status payload)
|
|
- `DELETE /course-management/sub-modules/:subModuleId`
|
|
- `POST /course-management/sub-courses/:subModuleId/thumbnail` (compat endpoint)
|
|
|
|
Wrappers:
|
|
- `updateSubModule(...)`
|
|
- `updateSubModuleStatus(...)`
|
|
- `deleteSubModule(...)`
|
|
- `updateSubModuleThumbnail(...)`
|
|
|
|
---
|
|
|
|
## 6) Video APIs (sub-module videos)
|
|
|
|
## 6.1 List videos for sub-module
|
|
- `GET /course-management/sub-modules/:subModuleId/videos`
|
|
- wrapper: `getVideosBySubModule(subModuleId)`
|
|
|
|
## 6.2 Create video
|
|
|
|
Two wrapper variants, same endpoint:
|
|
- `POST /course-management/sub-module-videos`
|
|
|
|
### Minimal variant
|
|
- `createSubCourseVideo({ sub_module_id|sub_course_id, title, description, video_url })`
|
|
|
|
### Extended variant
|
|
- `createCourseVideo({ sub_module_id|sub_course_id, title, description, video_url, duration, resolution?, visibility?, display_order?, status? })`
|
|
|
|
## 6.3 Update/delete video
|
|
- `PUT /course-management/sub-module-videos/:videoId`
|
|
- `DELETE /course-management/sub-module-videos/:videoId`
|
|
|
|
Wrappers:
|
|
- `updateSubCourseVideo(videoId, data)`
|
|
- `deleteSubCourseVideo(videoId)`
|
|
|
|
---
|
|
|
|
## 7) Practices and lessons
|
|
|
|
## 7.1 Practices by sub-module
|
|
- `GET /question-sets/by-owner?owner_type=SUB_MODULE&owner_id=:subModuleId`
|
|
- wrapper: `getPracticesBySubModule(subModuleId)`
|
|
|
|
## 7.2 Create practice (composed)
|
|
|
|
`createPractice(data)` does:
|
|
1. `POST /question-sets`
|
|
- `set_type: "PRACTICE"`
|
|
- `owner_type: "SUB_MODULE"`
|
|
- `owner_id: sub_module_id`
|
|
2. If step 1 succeeds, links to sub-module practice:
|
|
- `POST /course-management/sub-module-practices`
|
|
- includes `question_set_id` and intro metadata
|
|
|
|
## 7.3 Create lesson (composed)
|
|
|
|
`createLesson(data)` does:
|
|
1. `POST /question-sets`
|
|
- `set_type: "QUIZ"`
|
|
- `owner_type: "SUB_MODULE"`
|
|
2. Link question set as lesson:
|
|
- `POST /course-management/sub-module-lessons`
|
|
|
|
## 7.4 Practice update/delete/status
|
|
- `PUT /course-management/practices/:practiceId`
|
|
- `PUT /course-management/practices/:practiceId` (status)
|
|
- `DELETE /course-management/practices/:practiceId`
|
|
|
|
Wrappers:
|
|
- `updatePractice(...)`
|
|
- `updatePracticeStatus(...)`
|
|
- `deletePractice(...)`
|
|
|
|
---
|
|
|
|
## 8) Question sets and questions
|
|
|
|
## 8.1 Question sets
|
|
- `GET /question-sets` with optional query params
|
|
- `GET /question-sets/by-owner`
|
|
- `GET /question-sets/:id`
|
|
- `PUT /question-sets/:id`
|
|
- `DELETE /question-sets/:id`
|
|
- `POST /question-sets`
|
|
|
|
Wrappers:
|
|
- `getQuestionSets(params?)`
|
|
- `getQuestionSetsByOwner(ownerType, ownerId)`
|
|
- `getQuestionSetById(questionSetId)`
|
|
- `createQuestionSet(data)`
|
|
- `updateQuestionSet(questionSetId, partialData)`
|
|
- `deleteQuestionSet(questionSetId)`
|
|
|
|
## 8.2 Question list within set
|
|
- `GET /question-sets/:questionSetId/questions`
|
|
- `POST /question-sets/:questionSetId/questions` (add by question id)
|
|
|
|
Wrappers:
|
|
- `getQuestionSetQuestions(questionSetId)`
|
|
- `addQuestionToSet(questionSetId, { question_id, display_order? })`
|
|
|
|
## 8.3 Questions CRUD
|
|
- `GET /questions` (filters)
|
|
- `GET /questions/:questionId`
|
|
- `POST /questions`
|
|
- `PUT /questions/:questionId`
|
|
- `DELETE /questions/:questionId`
|
|
|
|
Wrappers:
|
|
- `getQuestions(params)`
|
|
- `getQuestionById(questionId)`
|
|
- `createQuestion(data)`
|
|
- `updateQuestion(questionId, data)`
|
|
- `deleteQuestion(questionId)`
|
|
|
|
## 8.4 Practice-question convenience wrappers
|
|
|
|
`createPracticeQuestion(data)`:
|
|
1. Creates question via `POST /questions`
|
|
2. Adds it to practice set via `POST /question-sets/:practiceId/questions`
|
|
|
|
`updatePracticeQuestion(questionId, data)`:
|
|
- maps to `PUT /questions/:questionId`
|
|
|
|
`deletePracticeQuestion(questionId)`:
|
|
- `DELETE /questions/:questionId`
|
|
|
|
## 8.5 Practice question listing endpoint variants
|
|
- `getPracticeQuestions(practiceId)` -> `GET /question-sets/:practiceId/questions`
|
|
- `getPracticeQuestionsByPractice(practiceId, params)` -> `GET /practices/:practiceId/questions`
|
|
|
|
Use the second when you need pagination/filtering by question type.
|
|
|
|
---
|
|
|
|
## 9) Human language specific APIs
|
|
|
|
## 9.1 Human language hierarchy
|
|
- `getHumanLanguageHierarchy()`
|
|
- Calls `GET /course-management/hierarchy`
|
|
- If backend already returns nested `sub_categories`, uses it directly.
|
|
- If backend returns flat rows, client builds nested structure and enriches each course by:
|
|
- requesting `/course-management/courses/:courseId/hierarchy`
|
|
- requesting `/question-sets/by-owner` per sub-module
|
|
- deriving lessons from question sets where `set_type = "QUIZ"`
|
|
|
|
This method is heavier than basic endpoints and can issue many requests.
|
|
|
|
## 9.2 Human language lessons by course+level
|
|
- `GET /course-management/human-language/courses/:courseId/lessons?cefr_level=...`
|
|
- wrapper: `getHumanLanguageLessonsByCourse(courseId, cefrLevel)`
|
|
|
|
## 9.3 Create human language lesson structure
|
|
|
|
`createHumanLanguageLesson(data)` is composed:
|
|
1. `POST /course-management/levels`
|
|
2. `POST /course-management/modules`
|
|
3. `POST /course-management/sub-modules`
|
|
|
|
---
|
|
|
|
## 10) Learning path and assessments
|
|
|
|
- `GET /course-management/courses/:courseId/learning-path`
|
|
- wrapper: `getLearningPath(courseId)`
|
|
|
|
- `GET /question-sets/sub-courses/:subModuleId/entry-assessment`
|
|
- wrapper: `getSubModuleEntryAssessment(subModuleId)`
|
|
|
|
---
|
|
|
|
## 11) Unsupported or stubbed features in current frontend API layer
|
|
|
|
The following wrappers are intentionally stubbed in frontend and return resolved promises (no real backend call):
|
|
- `getSubModulePrerequisites`
|
|
- `addSubModulePrerequisite`
|
|
- `removeSubModulePrerequisite`
|
|
- `reorderCategories`
|
|
- `reorderCourses`
|
|
- `reorderSubModules`
|
|
- `reorderVideos`
|
|
- `reorderPractices`
|
|
|
|
Implication:
|
|
- UI may appear to support these flows, but persistence is not implemented through backend yet.
|
|
|
|
---
|
|
|
|
## 12) Legacy endpoints still exposed (backward compatibility)
|
|
|
|
These are still present in `courses.api.ts` but marked deprecated in types:
|
|
- Programs APIs
|
|
- Old levels APIs
|
|
- Old modules APIs
|
|
- Practices by level/module APIs
|
|
|
|
Prefer unified hierarchy/sub-module/question-set APIs for new work.
|
|
|
|
---
|
|
|
|
## 13) Integration patterns and recommendations
|
|
|
|
## 13.1 Safe creation flows
|
|
- For practice/lesson creation, keep composed behavior:
|
|
- create question set first
|
|
- then link to sub-module entity
|
|
- Handle partial failure:
|
|
- if link step fails after question set creation, frontend should show recoverable error and optionally support manual relink.
|
|
|
|
## 13.2 Request normalization
|
|
- `getQuestionSetsResponse.data` can be either:
|
|
- raw array
|
|
- object with `question_sets`
|
|
- Normalize before rendering.
|
|
|
|
## 13.3 Question type mapping
|
|
- UI uses `"SHORT"`; backend commonly expects `"SHORT_ANSWER"`.
|
|
- Existing wrappers already map `"SHORT"` to `"SHORT_ANSWER"` on create/update practice question.
|
|
|
|
## 13.4 Media handling
|
|
- Prefer using `/files/upload` wrappers for all media.
|
|
- For Vimeo-backed responses, frontend typically consumes `embed_url` (and may append hash from page URL where applicable).
|
|
|
|
## 13.5 Retry behavior
|
|
- Some hierarchy fetches use single retry (`withSingleRetry`) for resiliency against transient auth/network race conditions.
|
|
|
|
---
|
|
|
|
## 14) Quick endpoint index
|
|
|
|
### Course management
|
|
- `GET /course-management/hierarchy`
|
|
- `POST /course-management/categories`
|
|
- `POST /course-management/sub-categories`
|
|
- `DELETE /course-management/categories/:id`
|
|
- `DELETE /course-management/sub-categories/:id`
|
|
- `POST /course-management/courses`
|
|
- `PUT /course-management/courses/:id`
|
|
- `DELETE /course-management/courses/:id`
|
|
- `POST /course-management/courses/:id/thumbnail`
|
|
- `GET /course-management/courses/:courseId/hierarchy`
|
|
- `POST /course-management/levels`
|
|
- `POST /course-management/modules`
|
|
- `PUT /course-management/levels/:id`
|
|
- `DELETE /course-management/levels/:id`
|
|
- `PUT /course-management/modules/:id`
|
|
- `DELETE /course-management/modules/:id`
|
|
- `POST /course-management/sub-modules`
|
|
- `PUT /course-management/sub-modules/:id`
|
|
- `DELETE /course-management/sub-modules/:id`
|
|
- `GET /course-management/sub-modules/:subModuleId/videos`
|
|
- `POST /course-management/sub-module-videos`
|
|
- `PUT /course-management/sub-module-videos/:id`
|
|
- `DELETE /course-management/sub-module-videos/:id`
|
|
- `POST /course-management/sub-module-practices`
|
|
- `POST /course-management/sub-module-lessons`
|
|
- `GET /course-management/courses/:courseId/learning-path`
|
|
- `GET /course-management/human-language/courses/:courseId/lessons`
|
|
|
|
### Question sets and questions
|
|
- `GET /question-sets`
|
|
- `GET /question-sets/by-owner`
|
|
- `GET /question-sets/:id`
|
|
- `POST /question-sets`
|
|
- `PUT /question-sets/:id`
|
|
- `DELETE /question-sets/:id`
|
|
- `GET /question-sets/:id/questions`
|
|
- `POST /question-sets/:id/questions`
|
|
- `GET /practices/:practiceId/questions`
|
|
- `GET /questions`
|
|
- `GET /questions/:id`
|
|
- `POST /questions`
|
|
- `PUT /questions/:id`
|
|
- `DELETE /questions/:id`
|
|
- `POST /questions/audio-answer`
|
|
|
|
### File/media
|
|
- `POST /files/upload`
|
|
- `GET /files/url`
|
|
- `GET /vimeo/sample`
|
|
- `POST /vimeo/uploads/pull`
|
|
|
|
---
|
|
|
|
## 15) Suggested frontend service contract shape
|
|
|
|
For any new frontend module, follow this contract:
|
|
- **Input DTOs**: UI-friendly types (can include UI aliases like `SHORT`)
|
|
- **Mapper layer**: convert UI DTOs to backend DTOs
|
|
- **Transport layer**: pure API calls
|
|
- **Normalizer layer**: normalize polymorphic responses (`array` vs `object`)
|
|
- **Error policy**:
|
|
- show user-actionable toast
|
|
- preserve enough context to retry failed composed steps
|
|
|
|
This keeps integration robust even with mixed legacy/unified backend surfaces.
|
|
|