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:
@@ -0,0 +1,410 @@
|
||||
import {
|
||||
ZPublicApiCourseParam,
|
||||
ZPublicApiCoursesQuery,
|
||||
ZPublicApiCreateCourse,
|
||||
ZPublicApiUpdateCourse,
|
||||
ZPublicApiUpdateCourseStructure
|
||||
} from '@cio/utils/validation/public-api';
|
||||
import {
|
||||
createPublicApiCourseService,
|
||||
deletePublicApiCourseService,
|
||||
exportCourseService,
|
||||
getCourseService,
|
||||
listCoursesService,
|
||||
listCourseStudentsService,
|
||||
updatePublicApiCourseService,
|
||||
updatePublicApiCourseStructureService
|
||||
} from '@api/services/v1/course';
|
||||
|
||||
import { Hono } from '@api/utils/hono';
|
||||
import { automationKeyMiddleware } from '@api/middlewares/automation-key';
|
||||
import { automationKeyScopesMiddleware } from '@api/middlewares/automation-key-scopes';
|
||||
import { handleError } from '@api/utils/errors';
|
||||
import { describeRoute, validator } from 'hono-openapi';
|
||||
|
||||
const CoursesListResponse = {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
success: { type: 'boolean' as const },
|
||||
data: { type: 'array' as const, items: { type: 'object' as const } }
|
||||
},
|
||||
required: ['success', 'data']
|
||||
};
|
||||
|
||||
const CourseDetailResponse = {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
success: { type: 'boolean' as const },
|
||||
data: { type: 'object' as const }
|
||||
},
|
||||
required: ['success', 'data']
|
||||
};
|
||||
|
||||
export const v1CoursesRouter = new Hono()
|
||||
.get(
|
||||
'/',
|
||||
automationKeyMiddleware,
|
||||
automationKeyScopesMiddleware(['public_api:*']),
|
||||
describeRoute({
|
||||
description: 'List courses for the authenticated organization',
|
||||
tags: ['Public API Courses'],
|
||||
responses: {
|
||||
200: {
|
||||
description: 'Courses returned successfully',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: CoursesListResponse
|
||||
}
|
||||
}
|
||||
},
|
||||
401: { description: 'Unauthorized' },
|
||||
403: { description: 'Forbidden' }
|
||||
}
|
||||
}),
|
||||
validator('query', ZPublicApiCoursesQuery),
|
||||
async (c) => {
|
||||
try {
|
||||
const orgId = c.get('orgId')!;
|
||||
const query = c.req.valid('query');
|
||||
const courses = await listCoursesService(orgId, query);
|
||||
|
||||
return c.json(
|
||||
{
|
||||
success: true,
|
||||
data: courses
|
||||
},
|
||||
200
|
||||
);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to list courses');
|
||||
}
|
||||
}
|
||||
)
|
||||
.post(
|
||||
'/',
|
||||
automationKeyMiddleware,
|
||||
automationKeyScopesMiddleware(['public_api:*']),
|
||||
describeRoute({
|
||||
description: 'Create a course for the authenticated organization',
|
||||
tags: ['Public API Courses'],
|
||||
responses: {
|
||||
201: {
|
||||
description: 'Course created successfully',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: CourseDetailResponse
|
||||
}
|
||||
}
|
||||
},
|
||||
400: { description: 'Invalid request body' },
|
||||
401: { description: 'Unauthorized' },
|
||||
403: { description: 'Forbidden' }
|
||||
}
|
||||
}),
|
||||
validator('json', ZPublicApiCreateCourse),
|
||||
async (c) => {
|
||||
try {
|
||||
const orgId = c.get('orgId')!;
|
||||
const actorId = c.get('actorId');
|
||||
const payload = c.req.valid('json');
|
||||
const course = await createPublicApiCourseService(orgId, actorId, payload);
|
||||
|
||||
return c.json(
|
||||
{
|
||||
success: true,
|
||||
data: course
|
||||
},
|
||||
201
|
||||
);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to create course');
|
||||
}
|
||||
}
|
||||
)
|
||||
.get(
|
||||
'/:courseId/students',
|
||||
automationKeyMiddleware,
|
||||
automationKeyScopesMiddleware(['public_api:*']),
|
||||
describeRoute({
|
||||
description: 'List enrolled students for a course',
|
||||
tags: ['Public API Courses'],
|
||||
responses: {
|
||||
200: {
|
||||
description: 'Course students returned successfully',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: CoursesListResponse
|
||||
}
|
||||
}
|
||||
},
|
||||
401: { description: 'Unauthorized' },
|
||||
403: { description: 'Forbidden' },
|
||||
404: { description: 'Course not found' }
|
||||
}
|
||||
}),
|
||||
validator('param', ZPublicApiCourseParam),
|
||||
async (c) => {
|
||||
try {
|
||||
const orgId = c.get('orgId')!;
|
||||
const params = c.req.valid('param');
|
||||
const students = await listCourseStudentsService(orgId, params);
|
||||
|
||||
return c.json(
|
||||
{
|
||||
success: true,
|
||||
data: students
|
||||
},
|
||||
200
|
||||
);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to list course students');
|
||||
}
|
||||
}
|
||||
)
|
||||
.get(
|
||||
'/:courseId/export',
|
||||
automationKeyMiddleware,
|
||||
automationKeyScopesMiddleware(['public_api:*']),
|
||||
describeRoute({
|
||||
description: 'Export a course structure snapshot',
|
||||
tags: ['Public API Courses'],
|
||||
responses: {
|
||||
200: {
|
||||
description: 'Course export returned successfully',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: CourseDetailResponse
|
||||
}
|
||||
}
|
||||
},
|
||||
401: { description: 'Unauthorized' },
|
||||
403: { description: 'Forbidden' },
|
||||
404: { description: 'Course not found' }
|
||||
}
|
||||
}),
|
||||
validator('param', ZPublicApiCourseParam),
|
||||
async (c) => {
|
||||
try {
|
||||
const orgId = c.get('orgId')!;
|
||||
const params = c.req.valid('param');
|
||||
const courseExport = await exportCourseService(orgId, params);
|
||||
|
||||
return c.json(
|
||||
{
|
||||
success: true,
|
||||
data: courseExport
|
||||
},
|
||||
200
|
||||
);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to export course');
|
||||
}
|
||||
}
|
||||
)
|
||||
.get(
|
||||
'/:courseId/structure',
|
||||
automationKeyMiddleware,
|
||||
automationKeyScopesMiddleware(['public_api:*']),
|
||||
describeRoute({
|
||||
description: 'Get a course structure snapshot',
|
||||
tags: ['Public API Courses'],
|
||||
responses: {
|
||||
200: {
|
||||
description: 'Course structure returned successfully',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: CourseDetailResponse
|
||||
}
|
||||
}
|
||||
},
|
||||
401: { description: 'Unauthorized' },
|
||||
403: { description: 'Forbidden' },
|
||||
404: { description: 'Course not found' }
|
||||
}
|
||||
}),
|
||||
validator('param', ZPublicApiCourseParam),
|
||||
async (c) => {
|
||||
try {
|
||||
const orgId = c.get('orgId')!;
|
||||
const params = c.req.valid('param');
|
||||
const courseStructure = await exportCourseService(orgId, params);
|
||||
|
||||
return c.json(
|
||||
{
|
||||
success: true,
|
||||
data: courseStructure
|
||||
},
|
||||
200
|
||||
);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to fetch course structure');
|
||||
}
|
||||
}
|
||||
)
|
||||
.put(
|
||||
'/:courseId/structure',
|
||||
automationKeyMiddleware,
|
||||
automationKeyScopesMiddleware(['public_api:*']),
|
||||
describeRoute({
|
||||
description: 'Synchronize a course structure using the draft payload shape',
|
||||
tags: ['Public API Courses'],
|
||||
responses: {
|
||||
200: {
|
||||
description: 'Course structure updated successfully',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: CourseDetailResponse
|
||||
}
|
||||
}
|
||||
},
|
||||
400: { description: 'Invalid request body' },
|
||||
401: { description: 'Unauthorized' },
|
||||
403: { description: 'Forbidden' },
|
||||
404: { description: 'Course not found' }
|
||||
}
|
||||
}),
|
||||
validator('param', ZPublicApiCourseParam),
|
||||
validator('json', ZPublicApiUpdateCourseStructure),
|
||||
async (c) => {
|
||||
try {
|
||||
const orgId = c.get('orgId')!;
|
||||
const actorId = c.get('actorId');
|
||||
const params = c.req.valid('param');
|
||||
const payload = c.req.valid('json');
|
||||
const result = await updatePublicApiCourseStructureService(orgId, actorId, params, payload);
|
||||
|
||||
return c.json(
|
||||
{
|
||||
success: true,
|
||||
data: result
|
||||
},
|
||||
200
|
||||
);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to update course structure');
|
||||
}
|
||||
}
|
||||
)
|
||||
.get(
|
||||
'/:courseId',
|
||||
automationKeyMiddleware,
|
||||
automationKeyScopesMiddleware(['public_api:*']),
|
||||
describeRoute({
|
||||
description: 'Get a single course by id',
|
||||
tags: ['Public API Courses'],
|
||||
responses: {
|
||||
200: {
|
||||
description: 'Course returned successfully',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: CourseDetailResponse
|
||||
}
|
||||
}
|
||||
},
|
||||
401: { description: 'Unauthorized' },
|
||||
403: { description: 'Forbidden' },
|
||||
404: { description: 'Course not found' }
|
||||
}
|
||||
}),
|
||||
validator('param', ZPublicApiCourseParam),
|
||||
async (c) => {
|
||||
try {
|
||||
const orgId = c.get('orgId')!;
|
||||
const params = c.req.valid('param');
|
||||
const course = await getCourseService(orgId, params);
|
||||
|
||||
return c.json(
|
||||
{
|
||||
success: true,
|
||||
data: course
|
||||
},
|
||||
200
|
||||
);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to fetch course');
|
||||
}
|
||||
}
|
||||
)
|
||||
.put(
|
||||
'/:courseId',
|
||||
automationKeyMiddleware,
|
||||
automationKeyScopesMiddleware(['public_api:*']),
|
||||
describeRoute({
|
||||
description: 'Update a course by id',
|
||||
tags: ['Public API Courses'],
|
||||
responses: {
|
||||
200: {
|
||||
description: 'Course updated successfully',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: CourseDetailResponse
|
||||
}
|
||||
}
|
||||
},
|
||||
400: { description: 'Invalid request body' },
|
||||
401: { description: 'Unauthorized' },
|
||||
403: { description: 'Forbidden' },
|
||||
404: { description: 'Course not found' }
|
||||
}
|
||||
}),
|
||||
validator('param', ZPublicApiCourseParam),
|
||||
validator('json', ZPublicApiUpdateCourse),
|
||||
async (c) => {
|
||||
try {
|
||||
const orgId = c.get('orgId')!;
|
||||
const params = c.req.valid('param');
|
||||
const payload = c.req.valid('json');
|
||||
const course = await updatePublicApiCourseService(orgId, params, payload);
|
||||
|
||||
return c.json(
|
||||
{
|
||||
success: true,
|
||||
data: course
|
||||
},
|
||||
200
|
||||
);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to update course');
|
||||
}
|
||||
}
|
||||
)
|
||||
.delete(
|
||||
'/:courseId',
|
||||
automationKeyMiddleware,
|
||||
automationKeyScopesMiddleware(['public_api:*']),
|
||||
describeRoute({
|
||||
description: 'Delete a course by id',
|
||||
tags: ['Public API Courses'],
|
||||
responses: {
|
||||
200: {
|
||||
description: 'Course deleted successfully',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: CourseDetailResponse
|
||||
}
|
||||
}
|
||||
},
|
||||
401: { description: 'Unauthorized' },
|
||||
403: { description: 'Forbidden' },
|
||||
404: { description: 'Course not found' }
|
||||
}
|
||||
}),
|
||||
validator('param', ZPublicApiCourseParam),
|
||||
async (c) => {
|
||||
try {
|
||||
const orgId = c.get('orgId')!;
|
||||
const params = c.req.valid('param');
|
||||
const course = await deletePublicApiCourseService(orgId, params);
|
||||
|
||||
return c.json(
|
||||
{
|
||||
success: true,
|
||||
data: course
|
||||
},
|
||||
200
|
||||
);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to delete course');
|
||||
}
|
||||
}
|
||||
);
|
||||
Reference in New Issue
Block a user