feat: certification rules, content sanitization, handle draft course updates and reset content, better redis logging

This commit is contained in:
Rotimi Best
2026-03-30 08:24:55 +01:00
parent ff2ad59377
commit 5a8f50f67b
131 changed files with 5643 additions and 2134 deletions
+32
View File
@@ -26,6 +26,7 @@ import {
getCourseProgress,
updateCourse
} from '@api/services/course/course';
import { assertCertificateDownloadAllowed, evaluateCourseCertification } from '@api/services/course/completion';
import { getCourseTags, replaceCourseTags } from '@api/services/tag';
import { Hono } from '@api/utils/hono';
@@ -393,6 +394,33 @@ export const courseRouter = new Hono()
}
}
)
/**
* GET /course/:courseId/certification-evaluation
* Full certification eligibility for the current user (blockers for UI).
*/
.get(
'/:courseId/certification-evaluation',
authMiddleware,
courseMemberMiddleware,
zValidator('param', ZCourseProgressParam),
async (c) => {
try {
const { courseId } = c.req.valid('param');
const user = c.get('user')!;
const data = await evaluateCourseCertification(courseId, user.id);
return c.json(
{
success: true,
data
},
200
);
} catch (error) {
return handleError(c, error, 'Failed to evaluate certification');
}
}
)
/**
* GET /course/:courseId/analytics
* Gets course analytics including student progress, completion rates, and grades
@@ -427,6 +455,10 @@ export const courseRouter = new Hono()
zValidator('param', ZCourseDownloadParam),
zValidator('json', ZCertificateDownload),
async (c) => {
const { courseId } = c.req.valid('param');
const user = c.get('user')!;
await assertCertificateDownloadAllowed(courseId, user.id);
const validatedData = c.req.valid('json');
const result = await generateCertificate(validatedData);
@@ -4,6 +4,7 @@ import {
ZCancelOrgPlan,
ZCreateOrgPlan,
ZCreateOrganization,
ZGetAudienceQuery,
ZGetCoursesBySiteName,
ZGetOrgSetup,
ZGetOrganizationCoursesQuery,
@@ -37,6 +38,7 @@ import {
getRecommendedCourses,
getUserAnalytics,
getUserEnrolledCourses,
removeAudienceMember,
removeTeamMember,
updateOrg,
updateOrgPlan
@@ -178,15 +180,18 @@ export const organizationRouter = new Hono()
* Gets organization audience (students)
* Requires authentication
*/
.get('/audience', authMiddleware, orgTeamMemberMiddleware, async (c) => {
.get('/audience', authMiddleware, orgTeamMemberMiddleware, zValidator('query', ZGetAudienceQuery), async (c) => {
try {
const orgId = c.req.header('cio-org-id')!;
const audience = await getOrgAudience(orgId);
const query = c.req.valid('query');
const audience = await getOrgAudience(orgId, query);
return c.json(
{
success: true,
data: audience
data: audience.items,
pagination: audience.pagination,
query: audience.query
},
200
);
@@ -194,6 +199,35 @@ export const organizationRouter = new Hono()
return handleError(c, error, 'Failed to fetch organization audience');
}
})
/**
* DELETE /organization/audience/:memberId
* Removes a student from the organization
*/
.delete(
'/audience/:memberId',
authMiddleware,
orgAdminMiddleware,
zValidator('param', ZRemoveTeamMember),
async (c) => {
try {
const orgId = c.req.header('cio-org-id')!;
const user = c.get('user')!;
const { memberId } = c.req.valid('param');
const deleted = await removeAudienceMember(orgId, memberId, user.id);
return c.json(
{
success: true,
data: deleted
},
200
);
} catch (error) {
return handleError(c, error, 'Failed to remove audience member');
}
}
)
/**
* POST /organization/audience/resend-invite
* Resends a student org invite email (new token; optional course metadata from last invite)
@@ -604,7 +638,7 @@ export const organizationRouter = new Hono()
.post(
'/audience/assign-courses',
authMiddleware,
orgAdminMiddleware,
orgTeamMemberMiddleware,
zValidator('json', ZAssignAudienceCourses),
async (c) => {
try {