feat: certification rules, content sanitization, handle draft course updates and reset content, better redis logging
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
export * from './array';
|
||||
export * from './fileValidation';
|
||||
export * from './sanitize';
|
||||
export * from './slug';
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
export const FORBID_TAGS = [
|
||||
'script',
|
||||
'iframe',
|
||||
'object',
|
||||
'embed',
|
||||
'form',
|
||||
'input',
|
||||
'textarea',
|
||||
'select',
|
||||
'button',
|
||||
'meta',
|
||||
'link',
|
||||
'svg',
|
||||
'math'
|
||||
] as const;
|
||||
|
||||
export const FORBID_ATTR = [
|
||||
'onerror',
|
||||
'onload',
|
||||
'onclick',
|
||||
'onmouseover',
|
||||
'onfocus',
|
||||
'onblur',
|
||||
'onchange',
|
||||
'onsubmit',
|
||||
'onreset',
|
||||
'onselect',
|
||||
'onunload'
|
||||
] as const;
|
||||
|
||||
export const ADD_ATTR = ['data-type', 'data-latex', 'colwidth'] as const;
|
||||
|
||||
export const ALLOWED_URI_REGEXP =
|
||||
/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i;
|
||||
|
||||
export function createSanitizeHtmlConfig() {
|
||||
return {
|
||||
FORBID_TAGS: [...FORBID_TAGS],
|
||||
FORBID_ATTR: [...FORBID_ATTR],
|
||||
ADD_ATTR: [...ADD_ATTR],
|
||||
KEEP_CONTENT: true,
|
||||
SANITIZE_DOM: true,
|
||||
ALLOW_DATA_ATTR: false,
|
||||
ALLOWED_URI_REGEXP
|
||||
};
|
||||
}
|
||||
|
||||
export function stripSvgDataUrls(html: string): string {
|
||||
return html.replace(/src\s*=\s*["']data:image\/svg[^"']*["']/gi, 'src=""');
|
||||
}
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user