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
+13 -2
View File
@@ -279,6 +279,18 @@ export const ZCourseLandingPageMetadataUpdate = z.object({
});
export type TCourseLandingPageMetadataUpdate = z.infer<typeof ZCourseLandingPageMetadataUpdate>;
export const ZCertificationSettings = z.object({
isDownloadable: z.boolean().optional(),
theme: z.string().optional(),
/** ISO 8601 datetime string or null to clear */
deadline: z.union([z.string().min(1), z.null()]).optional(),
threshold: z.number().int().min(0).max(100).optional(),
requiredExerciseId: z.union([z.string().uuid(), z.null()]).optional(),
exerciseMinScorePercent: z.number().int().min(0).max(100).nullable().optional(),
emailMessage: z.string().max(5000).nullable().optional()
});
export type TCertificationSettings = z.infer<typeof ZCertificationSettings>;
export const ZCourseUpdate = z.object({
title: z.string().min(1).optional(),
description: z.string().min(1).optional(),
@@ -290,8 +302,7 @@ export const ZCourseUpdate = z.object({
metadata: ZCourseMetadata.optional(),
cost: z.number().int().min(0).optional(),
currency: z.enum(['NGN', 'USD']).optional(),
isCertificateDownloadable: z.boolean().optional(),
certificateTheme: z.string().optional(),
certificate: ZCertificationSettings.optional(),
tagIds: z.array(z.uuid()).max(100).optional()
});
export type TCourseUpdate = z.infer<typeof ZCourseUpdate>;
@@ -155,6 +155,7 @@ export const ZExerciseUpdate = z.object({
order: z.number().int().optional(),
isUnlocked: z.boolean().optional(),
dueBy: z.string().optional(), // Changed from iso.datetime() to string to match frontend format
allowMultipleAttempts: z.boolean().optional(),
questions: z.array(ZExerciseUpdateQuestion).optional()
});
export type TExerciseUpdate = z.infer<typeof ZExerciseUpdate>;
@@ -1,5 +1,20 @@
import * as z from 'zod';
export const AudienceSortBy = z.enum(['createdAt', 'name', 'email']);
export const AudienceSortOrder = z.enum(['asc', 'desc']);
export const ZGetAudienceQuery = z.object({
page: z.coerce.number().int().min(1).default(1),
limit: z.coerce.number().int().min(1).max(100).default(20),
search: z.string().trim().optional(),
sortBy: AudienceSortBy.default('createdAt'),
sortOrder: AudienceSortOrder.default('desc')
});
export type TAudienceSortBy = z.infer<typeof AudienceSortBy>;
export type TAudienceSortOrder = z.infer<typeof AudienceSortOrder>;
export type TGetAudienceQuery = z.infer<typeof ZGetAudienceQuery>;
export const ZImportAudienceMembers = z.object({
recipientCsv: z.string().max(25000),
courseIds: z.array(z.string().uuid()).optional(),