diff --git a/Backend/src/controllers/AdminCourseApprovalController.ts b/Backend/src/controllers/AdminCourseApprovalController.ts index dac45be4..fc60b670 100644 --- a/Backend/src/controllers/AdminCourseApprovalController.ts +++ b/Backend/src/controllers/AdminCourseApprovalController.ts @@ -1,6 +1,7 @@ import { Body, Get, Path, Post, Request, Response, Route, Security, SuccessResponse, Tags } from 'tsoa'; import { ValidationError } from '../middleware/errorHandler'; import { AdminCourseApprovalService } from '../services/AdminCourseApproval.service'; +import { ApproveCourseValidator, RejectCourseValidator } from '../validators/AdminCourseApproval.validator'; import { ListPendingCoursesResponse, GetCourseDetailForAdminResponse, @@ -65,6 +66,13 @@ export class AdminCourseApprovalController { ): Promise { const token = request.headers.authorization?.replace('Bearer ', ''); if (!token) throw new ValidationError('No token provided'); + + // Validate body if provided + if (body) { + const { error } = ApproveCourseValidator.validate(body); + if (error) throw new ValidationError(error.details[0].message); + } + return await AdminCourseApprovalService.approveCourse(token, courseId, body?.comment); } @@ -87,6 +95,11 @@ export class AdminCourseApprovalController { ): Promise { const token = request.headers.authorization?.replace('Bearer ', ''); if (!token) throw new ValidationError('No token provided'); + + // Validate body + const { error } = RejectCourseValidator.validate(body); + if (error) throw new ValidationError(error.details[0].message); + return await AdminCourseApprovalService.rejectCourse(token, courseId, body.comment); } } diff --git a/Backend/src/controllers/CategoriesController.ts b/Backend/src/controllers/CategoriesController.ts index 3c99a3b5..81fd8b86 100644 --- a/Backend/src/controllers/CategoriesController.ts +++ b/Backend/src/controllers/CategoriesController.ts @@ -2,6 +2,7 @@ import { Get, Body, Post, Route, Tags, SuccessResponse, Response, Delete, Contro import { ValidationError } from '../middleware/errorHandler'; import { CategoryService } from '../services/categories.service'; import { createCategory, createCategoryResponse, deleteCategoryResponse, updateCategory, updateCategoryResponse, ListCategoriesResponse } from '../types/categories.type'; +import { CreateCategoryValidator, UpdateCategoryValidator } from '../validators/categories.validator'; @Route('api/categories') @Tags('Categories') @@ -27,6 +28,11 @@ export class CategoriesAdminController { @Response('401', 'Invalid or expired token') public async createCategory(@Request() request: any, @Body() body: createCategory): Promise { const token = request.headers.authorization?.replace('Bearer ', '') || ''; + + // Validate body + const { error } = CreateCategoryValidator.validate(body); + if (error) throw new ValidationError(error.details[0].message); + return await this.categoryService.createCategory(token, body); } @@ -36,6 +42,11 @@ export class CategoriesAdminController { @Response('401', 'Invalid or expired token') public async updateCategory(@Request() request: any, @Body() body: updateCategory): Promise { const token = request.headers.authorization?.replace('Bearer ', '') || ''; + + // Validate body + const { error } = UpdateCategoryValidator.validate(body); + if (error) throw new ValidationError(error.details[0].message); + return await this.categoryService.updateCategory(token, body.id, body); } @@ -45,6 +56,6 @@ export class CategoriesAdminController { @Response('401', 'Invalid or expired token') public async deleteCategory(@Request() request: any, @Path() id: number): Promise { const token = request.headers.authorization?.replace('Bearer ', '') || ''; - return await this.categoryService.deleteCategory(token,id); + return await this.categoryService.deleteCategory(token, id); } } \ No newline at end of file diff --git a/Backend/src/controllers/ChaptersLessonInstructorController.ts b/Backend/src/controllers/ChaptersLessonInstructorController.ts index f0bb43fe..7ba48f5c 100644 --- a/Backend/src/controllers/ChaptersLessonInstructorController.ts +++ b/Backend/src/controllers/ChaptersLessonInstructorController.ts @@ -27,6 +27,18 @@ import { UpdateQuizResponse, UpdateQuizBody, } from '../types/ChaptersLesson.typs'; +import { + CreateChapterValidator, + UpdateChapterValidator, + ReorderChapterValidator, + CreateLessonValidator, + UpdateLessonValidator, + ReorderLessonsValidator, + AddQuestionValidator, + UpdateQuestionValidator, + ReorderQuestionValidator, + UpdateQuizValidator +} from '../validators/ChaptersLesson.validator'; const chaptersLessonService = new ChaptersLessonService(); @@ -55,6 +67,10 @@ export class ChaptersLessonInstructorController { ): Promise { const token = request.headers.authorization?.replace('Bearer ', ''); if (!token) throw new ValidationError('No token provided'); + + const { error } = CreateChapterValidator.validate(body); + if (error) throw new ValidationError(error.details[0].message); + return await chaptersLessonService.createChapter({ token, course_id: courseId, @@ -82,6 +98,10 @@ export class ChaptersLessonInstructorController { ): Promise { const token = request.headers.authorization?.replace('Bearer ', ''); if (!token) throw new ValidationError('No token provided'); + + const { error } = UpdateChapterValidator.validate(body); + if (error) throw new ValidationError(error.details[0].message); + return await chaptersLessonService.updateChapter({ token, course_id: courseId, @@ -125,6 +145,10 @@ export class ChaptersLessonInstructorController { ): Promise { const token = request.headers.authorization?.replace('Bearer ', ''); if (!token) throw new ValidationError('No token provided'); + + const { error } = ReorderChapterValidator.validate(body); + if (error) throw new ValidationError(error.details[0].message); + return await chaptersLessonService.reorderChapter({ token, course_id: courseId, @@ -170,6 +194,10 @@ export class ChaptersLessonInstructorController { ): Promise { const token = request.headers.authorization?.replace('Bearer ', ''); if (!token) throw new ValidationError('No token provided'); + + const { error } = CreateLessonValidator.validate(body); + if (error) throw new ValidationError(error.details[0].message); + return await chaptersLessonService.createLesson({ token, course_id: courseId, @@ -197,6 +225,10 @@ export class ChaptersLessonInstructorController { ): Promise { const token = request.headers.authorization?.replace('Bearer ', ''); if (!token) throw new ValidationError('No token provided'); + + const { error } = UpdateLessonValidator.validate(body); + if (error) throw new ValidationError(error.details[0].message); + return await chaptersLessonService.updateLesson({ token, course_id: courseId, @@ -246,6 +278,10 @@ export class ChaptersLessonInstructorController { ): Promise { const token = request.headers.authorization?.replace('Bearer ', ''); if (!token) throw new ValidationError('No token provided'); + + const { error } = ReorderLessonsValidator.validate(body); + if (error) throw new ValidationError(error.details[0].message); + return await chaptersLessonService.reorderLessons({ token, course_id: courseId, @@ -275,6 +311,10 @@ export class ChaptersLessonInstructorController { ): Promise { const token = request.headers.authorization?.replace('Bearer ', ''); if (!token) throw new ValidationError('No token provided'); + + const { error } = AddQuestionValidator.validate(body); + if (error) throw new ValidationError(error.details[0].message); + return await chaptersLessonService.addQuestion({ token, course_id: courseId, @@ -300,6 +340,10 @@ export class ChaptersLessonInstructorController { ): Promise { const token = request.headers.authorization?.replace('Bearer ', ''); if (!token) throw new ValidationError('No token provided'); + + const { error } = UpdateQuestionValidator.validate(body); + if (error) throw new ValidationError(error.details[0].message); + return await chaptersLessonService.updateQuestion({ token, course_id: courseId, @@ -322,6 +366,10 @@ export class ChaptersLessonInstructorController { ): Promise { const token = request.headers.authorization?.replace('Bearer ', ''); if (!token) throw new ValidationError('No token provided'); + + const { error } = ReorderQuestionValidator.validate(body); + if (error) throw new ValidationError(error.details[0].message); + return await chaptersLessonService.reorderQuestion({ token, course_id: courseId, @@ -371,6 +419,10 @@ export class ChaptersLessonInstructorController { ): Promise { const token = request.headers.authorization?.replace('Bearer ', ''); if (!token) throw new ValidationError('No token provided'); + + const { error } = UpdateQuizValidator.validate(body); + if (error) throw new ValidationError(error.details[0].message); + return await chaptersLessonService.updateQuiz({ token, course_id: courseId, diff --git a/Backend/src/controllers/CoursesInstructorController.ts b/Backend/src/controllers/CoursesInstructorController.ts index 3be4ee47..3657e928 100644 --- a/Backend/src/controllers/CoursesInstructorController.ts +++ b/Backend/src/controllers/CoursesInstructorController.ts @@ -2,30 +2,28 @@ import { Get, Body, Post, Route, Tags, SuccessResponse, Response, Security, Put, import { ValidationError } from '../middleware/errorHandler'; import { CoursesInstructorService } from '../services/CoursesInstructor.service'; import { - createCourses, createCourseResponse, - GetMyCourseResponse, - ListMyCoursesInput, ListMyCourseResponse, - addinstructorCourseResponse, - removeinstructorCourseResponse, - setprimaryCourseInstructorResponse, + GetMyCourseResponse, UpdateMyCourse, UpdateMyCourseResponse, DeleteMyCourseResponse, submitCourseResponse, listinstructorCourseResponse, - GetCourseApprovalsResponse, - SearchInstructorResponse, + addinstructorCourseResponse, + removeinstructorCourseResponse, + setprimaryCourseInstructorResponse, GetEnrolledStudentsResponse, + GetEnrolledStudentDetailResponse, GetQuizScoresResponse, GetQuizAttemptDetailResponse, - GetEnrolledStudentDetailResponse, + GetCourseApprovalsResponse, + SearchInstructorResponse, GetCourseApprovalHistoryResponse, setCourseDraftResponse, CloneCourseResponse, } from '../types/CoursesInstructor.types'; -import { CreateCourseValidator } from "../validators/CoursesInstructor.validator"; +import { CreateCourseValidator, UpdateCourseValidator, CloneCourseValidator } from "../validators/CoursesInstructor.validator"; import jwt from 'jsonwebtoken'; import { config } from '../config'; @@ -104,9 +102,11 @@ export class CoursesInstructorController { @Response('404', 'Course not found') public async updateCourse(@Request() request: any, @Path() courseId: number, @Body() body: UpdateMyCourse): Promise { const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) { - throw new ValidationError('No token provided'); - } + if (!token) throw new ValidationError('No token provided'); + + const { error } = UpdateCourseValidator.validate(body.data); + if (error) throw new ValidationError(error.details[0].message); + return await CoursesInstructorService.updateCourse(token, courseId, body.data); } @@ -199,13 +199,16 @@ export class CoursesInstructorController { const token = request.headers.authorization?.replace('Bearer ', ''); if (!token) throw new ValidationError('No token provided'); - return await CoursesInstructorService.cloneCourse({ + const { error } = CloneCourseValidator.validate(body); + if (error) throw new ValidationError(error.details[0].message); + + const result = await CoursesInstructorService.cloneCourse({ token, course_id: courseId, title: body.title }); + return result; } - /** * ส่งคอร์สเพื่อขออนุมัติจากแอดมิน * Submit course for admin review and approval diff --git a/Backend/src/controllers/CoursesStudentController.ts b/Backend/src/controllers/CoursesStudentController.ts index afcf80b0..87a5a613 100644 --- a/Backend/src/controllers/CoursesStudentController.ts +++ b/Backend/src/controllers/CoursesStudentController.ts @@ -16,6 +16,7 @@ import { GetQuizAttemptsResponse, } from '../types/CoursesStudent.types'; import { EnrollmentStatus } from '@prisma/client'; +import { SaveVideoProgressValidator, SubmitQuizValidator } from '../validators/CoursesStudent.validator'; @Route('api/students') @Tags('CoursesStudent') @@ -149,9 +150,11 @@ export class CoursesStudentController { @Body() body: SaveVideoProgressBody ): Promise { const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) { - throw new ValidationError('No token provided'); - } + if (!token) throw new ValidationError('No token provided'); + + const { error } = SaveVideoProgressValidator.validate(body); + if (error) throw new ValidationError(error.details[0].message); + return await this.service.saveVideoProgress({ token, lesson_id: lessonId, @@ -225,9 +228,11 @@ export class CoursesStudentController { @Body() body: SubmitQuizBody ): Promise { const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) { - throw new ValidationError('No token provided'); - } + if (!token) throw new ValidationError('No token provided'); + + const { error } = SubmitQuizValidator.validate(body); + if (error) throw new ValidationError(error.details[0].message); + return await this.service.submitQuiz({ token, course_id: courseId, diff --git a/Backend/src/controllers/LessonsController.ts b/Backend/src/controllers/LessonsController.ts index f054ef4e..0323f4ab 100644 --- a/Backend/src/controllers/LessonsController.ts +++ b/Backend/src/controllers/LessonsController.ts @@ -11,6 +11,7 @@ import { YouTubeVideoResponse, SetYouTubeVideoBody, } from '../types/ChaptersLesson.typs'; +import { SetYouTubeVideoValidator } from '../validators/Lessons.validator'; const chaptersLessonService = new ChaptersLessonService(); @@ -213,12 +214,8 @@ export class LessonsController { const token = request.headers.authorization?.replace('Bearer ', ''); if (!token) throw new ValidationError('No token provided'); - if (!body.youtube_video_id) { - throw new ValidationError('YouTube video ID is required'); - } - if (!body.video_title) { - throw new ValidationError('Video title is required'); - } + const { error } = SetYouTubeVideoValidator.validate(body); + if (error) throw new ValidationError(error.details[0].message); return await chaptersLessonService.setYouTubeVideo({ token, diff --git a/Backend/src/controllers/announcementsController.ts b/Backend/src/controllers/announcementsController.ts index 6a4b901c..8ac03c70 100644 --- a/Backend/src/controllers/announcementsController.ts +++ b/Backend/src/controllers/announcementsController.ts @@ -1,6 +1,7 @@ import { Body, Delete, Get, Path, Post, Put, Query, Request, Response, Route, Security, SuccessResponse, Tags, UploadedFile, UploadedFiles, FormField } from 'tsoa'; import { ValidationError } from '../middleware/errorHandler'; import { AnnouncementsService } from '../services/announcements.service'; +import { CreateAnnouncementValidator, UpdateAnnouncementValidator } from '../validators/announcements.validator'; import { ListAnnouncementResponse, CreateAnnouncementResponse, @@ -68,6 +69,10 @@ export class AnnouncementsController { // Parse JSON data field const parsed = JSON.parse(data) as CreateAnnouncementBody; + // Validate parsed data + const { error } = CreateAnnouncementValidator.validate(parsed); + if (error) throw new ValidationError(error.details[0].message); + return await announcementsService.createAnnouncement({ token, course_id: courseId, @@ -100,6 +105,11 @@ export class AnnouncementsController { ): Promise { const token = request.headers.authorization?.replace('Bearer ', ''); if (!token) throw new ValidationError('No token provided'); + + // Validate body + const { error } = UpdateAnnouncementValidator.validate(body); + if (error) throw new ValidationError(error.details[0].message); + return await announcementsService.updateAnnouncement({ token, course_id: courseId, diff --git a/Backend/src/validators/AdminCourseApproval.validator.ts b/Backend/src/validators/AdminCourseApproval.validator.ts new file mode 100644 index 00000000..89e0a284 --- /dev/null +++ b/Backend/src/validators/AdminCourseApproval.validator.ts @@ -0,0 +1,30 @@ +import Joi from 'joi'; + +/** + * Validator for approving a course + * Comment is optional + */ +export const ApproveCourseValidator = Joi.object({ + comment: Joi.string() + .max(1000) + .optional() + .messages({ + 'string.max': 'Comment must not exceed 1000 characters' + }) +}); + +/** + * Validator for rejecting a course + * Comment is required when rejecting + */ +export const RejectCourseValidator = Joi.object({ + comment: Joi.string() + .min(10) + .max(1000) + .required() + .messages({ + 'string.min': 'Comment must be at least 10 characters when rejecting a course', + 'string.max': 'Comment must not exceed 1000 characters', + 'any.required': 'Comment is required when rejecting a course' + }) +}); diff --git a/Backend/src/validators/ChaptersLesson.validator.ts b/Backend/src/validators/ChaptersLesson.validator.ts new file mode 100644 index 00000000..933a3e1a --- /dev/null +++ b/Backend/src/validators/ChaptersLesson.validator.ts @@ -0,0 +1,186 @@ +import Joi from 'joi'; + +// Multi-language validation schema +const multiLangSchema = Joi.object({ + th: Joi.string().required().messages({ + 'any.required': 'Thai text is required' + }), + en: Joi.string().required().messages({ + 'any.required': 'English text is required' + }) +}).required(); + +const multiLangOptionalSchema = Joi.object({ + th: Joi.string().optional(), + en: Joi.string().optional() +}).optional(); + +// ============================================ +// Chapter Validators +// ============================================ + +/** + * Validator for creating a chapter + */ +export const CreateChapterValidator = Joi.object({ + title: multiLangSchema.messages({ + 'any.required': 'Title is required' + }), + description: multiLangOptionalSchema, + sort_order: Joi.number().integer().min(0).optional() +}); + +/** + * Validator for updating a chapter + */ +export const UpdateChapterValidator = Joi.object({ + title: multiLangOptionalSchema, + description: multiLangOptionalSchema, + sort_order: Joi.number().integer().min(0).optional(), + is_published: Joi.boolean().optional() +}); + +/** + * Validator for reordering a chapter + */ +export const ReorderChapterValidator = Joi.object({ + sort_order: Joi.number().integer().min(0).required().messages({ + 'any.required': 'Sort order is required', + 'number.min': 'Sort order must be at least 0' + }) +}); + +// ============================================ +// Lesson Validators +// ============================================ + +/** + * Validator for creating a lesson + */ +export const CreateLessonValidator = Joi.object({ + title: multiLangSchema.messages({ + 'any.required': 'Title is required' + }), + content: multiLangOptionalSchema, + type: Joi.string().valid('VIDEO', 'QUIZ').required().messages({ + 'any.only': 'Type must be either VIDEO or QUIZ', + 'any.required': 'Type is required' + }), + sort_order: Joi.number().integer().min(0).optional() +}); + +/** + * Validator for updating a lesson + */ +export const UpdateLessonValidator = Joi.object({ + title: multiLangOptionalSchema, + content: multiLangOptionalSchema, + duration_minutes: Joi.number().min(0).optional().messages({ + 'number.min': 'Duration must be at least 0' + }), + sort_order: Joi.number().integer().min(0).optional(), + prerequisite_lesson_ids: Joi.array().items(Joi.number().integer().positive()).optional(), + is_published: Joi.boolean().optional() +}); + +/** + * Validator for reordering lessons + */ +export const ReorderLessonsValidator = Joi.object({ + lesson_id: Joi.number().integer().positive().required().messages({ + 'any.required': 'Lesson ID is required' + }), + sort_order: Joi.number().integer().min(0).required().messages({ + 'any.required': 'Sort order is required' + }) +}); + +// ============================================ +// Quiz Question Validators +// ============================================ + +/** + * Validator for quiz choice + */ +const QuizChoiceValidator = Joi.object({ + text: multiLangSchema.messages({ + 'any.required': 'Choice text is required' + }), + is_correct: Joi.boolean().required().messages({ + 'any.required': 'is_correct is required' + }), + sort_order: Joi.number().integer().min(0).optional() +}); + +/** + * Validator for adding a question to a quiz + */ +export const AddQuestionValidator = Joi.object({ + question: multiLangSchema.messages({ + 'any.required': 'Question is required' + }), + explanation: multiLangOptionalSchema, + question_type: Joi.string() + .valid('MULTIPLE_CHOICE', 'TRUE_FALSE', 'SHORT_ANSWER') + .required() + .messages({ + 'any.only': 'Question type must be MULTIPLE_CHOICE, TRUE_FALSE, or SHORT_ANSWER', + 'any.required': 'Question type is required' + }), + sort_order: Joi.number().integer().min(0).optional(), + choices: Joi.array().items(QuizChoiceValidator).min(1).optional().messages({ + 'array.min': 'At least one choice is required for multiple choice questions' + }) +}); + +/** + * Validator for updating a question + */ +export const UpdateQuestionValidator = Joi.object({ + question: multiLangOptionalSchema, + explanation: multiLangOptionalSchema, + question_type: Joi.string() + .valid('MULTIPLE_CHOICE', 'TRUE_FALSE', 'SHORT_ANSWER') + .optional() + .messages({ + 'any.only': 'Question type must be MULTIPLE_CHOICE, TRUE_FALSE, or SHORT_ANSWER' + }), + sort_order: Joi.number().integer().min(0).optional(), + choices: Joi.array().items(QuizChoiceValidator).min(1).optional().messages({ + 'array.min': 'At least one choice is required' + }) +}); + +/** + * Validator for reordering a question + */ +export const ReorderQuestionValidator = Joi.object({ + sort_order: Joi.number().integer().min(0).required().messages({ + 'any.required': 'Sort order is required', + 'number.min': 'Sort order must be at least 0' + }) +}); + +// ============================================ +// Quiz Settings Validator +// ============================================ + +/** + * Validator for updating quiz settings + */ +export const UpdateQuizValidator = Joi.object({ + title: multiLangOptionalSchema, + description: multiLangOptionalSchema, + passing_score: Joi.number().min(0).max(100).optional().messages({ + 'number.min': 'Passing score must be at least 0', + 'number.max': 'Passing score must not exceed 100' + }), + time_limit: Joi.number().min(0).optional().messages({ + 'number.min': 'Time limit must be at least 0' + }), + shuffle_questions: Joi.boolean().optional(), + shuffle_choices: Joi.boolean().optional(), + show_answers_after_completion: Joi.boolean().optional(), + is_skippable: Joi.boolean().optional(), + allow_multiple_attempts: Joi.boolean().optional() +}); diff --git a/Backend/src/validators/CoursesInstructor.validator.ts b/Backend/src/validators/CoursesInstructor.validator.ts index fe971950..cbde5802 100644 --- a/Backend/src/validators/CoursesInstructor.validator.ts +++ b/Backend/src/validators/CoursesInstructor.validator.ts @@ -20,3 +20,38 @@ export const CreateCourseValidator = Joi.object({ is_free: Joi.boolean().required(), have_certificate: Joi.boolean().required(), }); + +/** + * Validator for updating a course + */ +export const UpdateCourseValidator = Joi.object({ + category_id: Joi.number().optional(), + title: Joi.object({ + th: Joi.string().optional(), + en: Joi.string().optional(), + }).optional(), + slug: Joi.string().optional(), + description: Joi.object({ + th: Joi.string().optional(), + en: Joi.string().optional(), + }).optional(), + price: Joi.number().optional(), + is_free: Joi.boolean().optional(), + have_certificate: Joi.boolean().optional(), +}); + +/** + * Validator for cloning a course + */ +export const CloneCourseValidator = Joi.object({ + title: Joi.object({ + th: Joi.string().required().messages({ + 'any.required': 'Thai title is required' + }), + en: Joi.string().required().messages({ + 'any.required': 'English title is required' + }) + }).required().messages({ + 'any.required': 'Title is required' + }) +}); diff --git a/Backend/src/validators/CoursesStudent.validator.ts b/Backend/src/validators/CoursesStudent.validator.ts new file mode 100644 index 00000000..424c35fe --- /dev/null +++ b/Backend/src/validators/CoursesStudent.validator.ts @@ -0,0 +1,38 @@ +import Joi from 'joi'; + +/** + * Validator for saving video progress + */ +export const SaveVideoProgressValidator = Joi.object({ + video_progress_seconds: Joi.number().min(0).required().messages({ + 'any.required': 'Video progress seconds is required', + 'number.min': 'Video progress must be at least 0' + }), + video_duration_seconds: Joi.number().min(0).optional().messages({ + 'number.min': 'Video duration must be at least 0' + }) +}); + +/** + * Validator for quiz answer + */ +const QuizAnswerValidator = Joi.object({ + question_id: Joi.number().integer().positive().required().messages({ + 'any.required': 'Question ID is required', + 'number.positive': 'Question ID must be positive' + }), + choice_id: Joi.number().integer().positive().required().messages({ + 'any.required': 'Choice ID is required', + 'number.positive': 'Choice ID must be positive' + }) +}); + +/** + * Validator for submitting quiz answers + */ +export const SubmitQuizValidator = Joi.object({ + answers: Joi.array().items(QuizAnswerValidator).min(1).required().messages({ + 'any.required': 'Answers are required', + 'array.min': 'At least one answer is required' + }) +}); diff --git a/Backend/src/validators/Lessons.validator.ts b/Backend/src/validators/Lessons.validator.ts new file mode 100644 index 00000000..4161ec53 --- /dev/null +++ b/Backend/src/validators/Lessons.validator.ts @@ -0,0 +1,15 @@ +import Joi from 'joi'; + +/** + * Validator for setting YouTube video + */ +export const SetYouTubeVideoValidator = Joi.object({ + youtube_video_id: Joi.string().required().messages({ + 'any.required': 'YouTube video ID is required', + 'string.empty': 'YouTube video ID cannot be empty' + }), + video_title: Joi.string().required().messages({ + 'any.required': 'Video title is required', + 'string.empty': 'Video title cannot be empty' + }) +}); diff --git a/Backend/src/validators/announcements.validator.ts b/Backend/src/validators/announcements.validator.ts new file mode 100644 index 00000000..bd9ad945 --- /dev/null +++ b/Backend/src/validators/announcements.validator.ts @@ -0,0 +1,72 @@ +import Joi from 'joi'; + +/** + * Validator for creating an announcement + */ +export const CreateAnnouncementValidator = Joi.object({ + title: Joi.object({ + th: Joi.string().required().messages({ + 'any.required': 'Thai title is required' + }), + en: Joi.string().required().messages({ + 'any.required': 'English title is required' + }) + }).required().messages({ + 'any.required': 'Title is required' + }), + content: Joi.object({ + th: Joi.string().required().messages({ + 'any.required': 'Thai content is required' + }), + en: Joi.string().required().messages({ + 'any.required': 'English content is required' + }) + }).required().messages({ + 'any.required': 'Content is required' + }), + status: Joi.string() + .valid('DRAFT', 'PUBLISHED', 'ARCHIVED') + .required() + .messages({ + 'any.only': 'Status must be one of: DRAFT, PUBLISHED, ARCHIVED', + 'any.required': 'Status is required' + }), + is_pinned: Joi.boolean() + .required() + .messages({ + 'any.required': 'is_pinned is required' + }), + published_at: Joi.string() + .isoDate() + .optional() + .messages({ + 'string.isoDate': 'published_at must be a valid ISO date string' + }) +}); + +/** + * Validator for updating an announcement + */ +export const UpdateAnnouncementValidator = Joi.object({ + title: Joi.object({ + th: Joi.string().optional(), + en: Joi.string().optional() + }).optional(), + content: Joi.object({ + th: Joi.string().optional(), + en: Joi.string().optional() + }).optional(), + status: Joi.string() + .valid('DRAFT', 'PUBLISHED', 'ARCHIVED') + .optional() + .messages({ + 'any.only': 'Status must be one of: DRAFT, PUBLISHED, ARCHIVED' + }), + is_pinned: Joi.boolean().optional(), + published_at: Joi.string() + .isoDate() + .optional() + .messages({ + 'string.isoDate': 'published_at must be a valid ISO date string' + }) +}); diff --git a/Backend/src/validators/categories.validator.ts b/Backend/src/validators/categories.validator.ts new file mode 100644 index 00000000..521c9faf --- /dev/null +++ b/Backend/src/validators/categories.validator.ts @@ -0,0 +1,58 @@ +import Joi from 'joi'; + +/** + * Validator for creating a category + */ +export const CreateCategoryValidator = Joi.object({ + name: Joi.object({ + th: Joi.string().required().messages({ + 'any.required': 'Thai name is required' + }), + en: Joi.string().required().messages({ + 'any.required': 'English name is required' + }) + }).required().messages({ + 'any.required': 'Name is required' + }), + slug: Joi.string() + .pattern(/^[a-z0-9]+(?:-[a-z0-9]+)*$/) + .required() + .messages({ + 'string.pattern.base': 'Slug must be lowercase with hyphens (e.g., web-development)', + 'any.required': 'Slug is required' + }), + description: Joi.object({ + th: Joi.string().required().messages({ + 'any.required': 'Thai description is required' + }), + en: Joi.string().required().messages({ + 'any.required': 'English description is required' + }) + }).required().messages({ + 'any.required': 'Description is required' + }), + created_by: Joi.number().optional() +}); + +/** + * Validator for updating a category + */ +export const UpdateCategoryValidator = Joi.object({ + id: Joi.number().required().messages({ + 'any.required': 'Category ID is required' + }), + name: Joi.object({ + th: Joi.string().optional(), + en: Joi.string().optional() + }).optional(), + slug: Joi.string() + .pattern(/^[a-z0-9]+(?:-[a-z0-9]+)*$/) + .optional() + .messages({ + 'string.pattern.base': 'Slug must be lowercase with hyphens (e.g., web-development)' + }), + description: Joi.object({ + th: Joi.string().optional(), + en: Joi.string().optional() + }).optional() +});