212 lines
7.3 KiB
TypeScript
212 lines
7.3 KiB
TypeScript
import { ErrorCodes, handleError } from '@api/utils/errors';
|
|||
|
|
import {
|
||
|
|
ZExerciseCreate,
|
||
|
|
ZExerciseFromTemplate,
|
||
|
|
ZExerciseGetParam,
|
||
|
|
ZExerciseListQuery,
|
||
|
|
ZExerciseSubmissionCreate,
|
||
|
|
ZExerciseUpdate
|
||
|
|
} from '@cio/utils/validation/exercise';
|
||
|
|
import { ZTemplateById, ZTemplateByTag } from '@cio/utils/validation/mocks';
|
||
|
|
import {
|
||
|
|
createExercise,
|
||
|
|
createExerciseFromTemplate,
|
||
|
|
deleteExerciseService,
|
||
|
|
getExercise,
|
||
|
|
listExercises,
|
||
|
|
updateExerciseService
|
||
|
|
} from '@api/services/exercise/exercise';
|
||
|
|
import { fetchAllTemplatesMetadata, fetchTemplateById, fetchTemplatesByTag } from '@api/services/exercise/template';
|
||
|
|
import { getGroupMemberIdByCourseAndProfile, isCourseTeamMemberOrOrgAdmin } from '@cio/db/queries/group';
|
||
|
|
|
||
|
|
import { Hono } from '@api/utils/hono';
|
||
|
|
import { authMiddleware } from '@api/middlewares/auth';
|
||
|
|
import { courseMemberMiddleware } from '@api/middlewares/course-member';
|
||
|
|
import { createSubmissionService, listExerciseSubmissionsOverview } from '@api/services/submission';
|
||
|
|
import { zValidator } from '@hono/zod-validator';
|
||
|
|
|
||
|
|
export const exerciseRouter = new Hono()
|
||
|
|
// Exercise CRUD routes
|
||
|
|
.get('/', authMiddleware, courseMemberMiddleware, zValidator('query', ZExerciseListQuery), async (c) => {
|
||
|
|
try {
|
||
|
|
const courseId = c.req.param('courseId')!;
|
||
|
|
const user = c.get('user')!;
|
||
|
|
const { lessonId, sectionId } = c.req.valid('query');
|
||
|
|
const exercises = await listExercises(courseId, { lessonId, sectionId }, user.id);
|
||
|
|
|
||
|
|
return c.json({ success: true, data: exercises }, 200);
|
||
|
|
} catch (error) {
|
||
|
|
return handleError(c, error, 'Failed to list exercises');
|
||
|
|
}
|
||
|
|
})
|
||
|
|
.get('/:exerciseId/submissions', authMiddleware, courseMemberMiddleware, async (c) => {
|
||
|
|
try {
|
||
|
|
const courseId = c.req.param('courseId')!;
|
||
|
|
const exerciseId = c.req.param('exerciseId')!;
|
||
|
|
const user = c.get('user')!;
|
||
|
|
|
||
|
|
const data = await listExerciseSubmissionsOverview(courseId, exerciseId, user.id);
|
||
|
|
return c.json({ success: true, data }, 200);
|
||
|
|
} catch (error) {
|
||
|
|
return handleError(c, error, 'Failed to fetch exercise submissions overview');
|
||
|
|
}
|
||
|
|
})
|
||
|
|
.get('/:exerciseId', authMiddleware, courseMemberMiddleware, zValidator('param', ZExerciseGetParam), async (c) => {
|
||
|
|
try {
|
||
|
|
const { exerciseId } = c.req.valid('param');
|
||
|
|
const user = c.get('user')!;
|
||
|
|
const exercise = await getExercise(exerciseId, undefined, user.id);
|
||
|
|
|
||
|
|
return c.json({ success: true, data: exercise }, 200);
|
||
|
|
} catch (error) {
|
||
|
|
return handleError(c, error, 'Failed to fetch exercise');
|
||
|
|
}
|
||
|
|
})
|
||
|
|
.post('/', authMiddleware, courseMemberMiddleware, zValidator('json', ZExerciseCreate), async (c) => {
|
||
|
|
try {
|
||
|
|
const data = c.req.valid('json');
|
||
|
|
|
||
|
|
const exercise = await createExercise(data);
|
||
|
|
|
||
|
|
return c.json({ success: true, data: exercise }, 201);
|
||
|
|
} catch (error) {
|
||
|
|
return handleError(c, error, 'Failed to create exercise');
|
||
|
|
}
|
||
|
|
})
|
||
|
|
/**
|
||
|
|
* POST /course/:courseId/exercise/from-template
|
||
|
|
* Creates an exercise from a template
|
||
|
|
* Requires authentication and course membership (admin/tutor role)
|
||
|
|
*/
|
||
|
|
.post(
|
||
|
|
'/from-template',
|
||
|
|
authMiddleware,
|
||
|
|
courseMemberMiddleware,
|
||
|
|
zValidator('json', ZExerciseFromTemplate),
|
||
|
|
async (c) => {
|
||
|
|
try {
|
||
|
|
const courseId = c.req.param('courseId')!;
|
||
|
|
const { lessonId, sectionId, order, templateId } = c.req.valid('json');
|
||
|
|
|
||
|
|
// Fetch template from database
|
||
|
|
const template = await fetchTemplateById(templateId);
|
||
|
|
if (!template) {
|
||
|
|
return c.json({ success: false, error: 'Template not found' }, 404);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!template.questionnaire) {
|
||
|
|
return c.json({ success: false, error: 'Template is missing questionnaire data' }, 400);
|
||
|
|
}
|
||
|
|
|
||
|
|
const exercise = await createExerciseFromTemplate(courseId, lessonId, sectionId, order, template);
|
||
|
|
|
||
|
|
return c.json({ success: true, data: exercise }, 201);
|
||
|
|
} catch (error) {
|
||
|
|
return handleError(c, error, 'Failed to create exercise from template');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
)
|
||
|
|
.put(
|
||
|
|
'/:exerciseId',
|
||
|
|
authMiddleware,
|
||
|
|
courseMemberMiddleware,
|
||
|
|
zValidator('param', ZExerciseGetParam),
|
||
|
|
zValidator('json', ZExerciseUpdate),
|
||
|
|
async (c) => {
|
||
|
|
try {
|
||
|
|
const { exerciseId } = c.req.valid('param');
|
||
|
|
const data = c.req.valid('json');
|
||
|
|
const courseId = c.req.param('courseId')!;
|
||
|
|
const user = c.get('user')!;
|
||
|
|
|
||
|
|
if (data.isUnlocked !== undefined) {
|
||
|
|
const isAuthorized = await isCourseTeamMemberOrOrgAdmin(courseId, user.id);
|
||
|
|
|
||
|
|
if (!isAuthorized) {
|
||
|
|
return c.json(
|
||
|
|
{
|
||
|
|
success: false,
|
||
|
|
error: 'Unauthorized',
|
||
|
|
code: ErrorCodes.UNAUTHORIZED
|
||
|
|
},
|
||
|
|
403
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const exercise = await updateExerciseService(exerciseId, data);
|
||
|
|
|
||
|
|
return c.json({ success: true, data: exercise }, 200);
|
||
|
|
} catch (error) {
|
||
|
|
return handleError(c, error, 'Failed to update exercise');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
)
|
||
|
|
.delete('/:exerciseId', authMiddleware, courseMemberMiddleware, zValidator('param', ZExerciseGetParam), async (c) => {
|
||
|
|
try {
|
||
|
|
const { exerciseId } = c.req.valid('param');
|
||
|
|
const exercise = await deleteExerciseService(exerciseId);
|
||
|
|
|
||
|
|
return c.json({ success: true, data: exercise }, 200);
|
||
|
|
} catch (error) {
|
||
|
|
return handleError(c, error, 'Failed to delete exercise');
|
||
|
|
}
|
||
|
|
})
|
||
|
|
// Exercise Submission routes
|
||
|
|
.post(
|
||
|
|
'/:exerciseId/submission',
|
||
|
|
authMiddleware,
|
||
|
|
courseMemberMiddleware,
|
||
|
|
zValidator('param', ZExerciseGetParam),
|
||
|
|
zValidator('json', ZExerciseSubmissionCreate),
|
||
|
|
async (c) => {
|
||
|
|
try {
|
||
|
|
const user = c.get('user')!;
|
||
|
|
const courseId = c.req.param('courseId')!;
|
||
|
|
const { exerciseId } = c.req.valid('param');
|
||
|
|
const { answers } = c.req.valid('json');
|
||
|
|
|
||
|
|
const groupMemberId = await getGroupMemberIdByCourseAndProfile(courseId, user.id);
|
||
|
|
if (!groupMemberId) {
|
||
|
|
return c.json({ success: false, error: 'User is not a member of this course' }, 403);
|
||
|
|
}
|
||
|
|
|
||
|
|
const submission = await createSubmissionService(courseId, exerciseId, groupMemberId, answers);
|
||
|
|
|
||
|
|
return c.json({ success: true, data: submission }, 201);
|
||
|
|
} catch (error) {
|
||
|
|
return handleError(c, error, 'Failed to submit exercise');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
)
|
||
|
|
// Template routes
|
||
|
|
.get('/template', authMiddleware, courseMemberMiddleware, async (c) => {
|
||
|
|
try {
|
||
|
|
const result = await fetchAllTemplatesMetadata();
|
||
|
|
|
||
|
|
return c.json({ success: true, data: result }, 200);
|
||
|
|
} catch (error) {
|
||
|
|
return handleError(c, error, 'Failed to load template metadata');
|
||
|
|
}
|
||
|
|
})
|
||
|
|
.get('/template/:id', authMiddleware, courseMemberMiddleware, zValidator('param', ZTemplateById), async (c) => {
|
||
|
|
try {
|
||
|
|
const { id } = c.req.valid('param');
|
||
|
|
|
||
|
|
const result = await fetchTemplateById(id);
|
||
|
|
return c.json({ success: true, data: result }, 200);
|
||
|
|
} catch (error) {
|
||
|
|
return handleError(c, error, 'Failed to load template');
|
||
|
|
}
|
||
|
|
})
|
||
|
|
.get('/template/tag/:tag', authMiddleware, courseMemberMiddleware, zValidator('param', ZTemplateByTag), async (c) => {
|
||
|
|
try {
|
||
|
|
const { tag } = c.req.valid('param');
|
||
|
|
|
||
|
|
const result = await fetchTemplatesByTag(tag);
|
||
|
|
return c.json({ success: true, data: result }, 200);
|
||
|
|
} catch (error) {
|
||
|
|
return handleError(c, error, 'Failed to load template');
|
||
|
|
}
|
||
|
|
});
|