package education import ( "net/http" "strconv" "time" "veza-backend-api/internal/common" "veza-backend-api/internal/core/education" "veza-backend-api/internal/response" "github.com/gin-gonic/gin" "go.uber.org/zap" ) // Handler gère les requêtes HTTP pour l'éducation type Handler struct { courseManager *education.CourseManager tutorialManager *education.TutorialManager logger *zap.Logger } // NewHandler crée un nouveau handler d'éducation func NewHandler(courseManager *education.CourseManager, tutorialManager *education.TutorialManager, logger *zap.Logger) *Handler { return &Handler{ courseManager: courseManager, tutorialManager: tutorialManager, logger: logger, } } // Request/Response structures type CreateCourseRequest struct { Title string `json:"title" binding:"required"` Description string `json:"description" binding:"required"` Instructor string `json:"instructor" binding:"required"` Category string `json:"category" binding:"required"` Level education.CourseLevel `json:"level" binding:"required"` Duration time.Duration `json:"duration" binding:"required"` Price float64 `json:"price"` Language string `json:"language" binding:"required"` Tags []string `json:"tags"` } type UpdateCourseRequest struct { Title *string `json:"title"` Description *string `json:"description"` Instructor *string `json:"instructor"` Category *string `json:"category"` Level *education.CourseLevel `json:"level"` Duration *time.Duration `json:"duration"` Price *float64 `json:"price"` Language *string `json:"language"` IsPublished *bool `json:"is_published"` Tags []string `json:"tags"` } type CreateTutorialRequest struct { Title string `json:"title" binding:"required"` Description string `json:"description" binding:"required"` Author string `json:"author" binding:"required"` Category string `json:"category" binding:"required"` VideoURL string `json:"video_url" binding:"required"` Thumbnail string `json:"thumbnail"` Duration time.Duration `json:"duration" binding:"required"` Quality education.VideoQuality `json:"quality" binding:"required"` Language string `json:"language" binding:"required"` IsFree bool `json:"is_free"` Tags []string `json:"tags"` } type UpdateTutorialRequest struct { Title *string `json:"title"` Description *string `json:"description"` Author *string `json:"author"` Category *string `json:"category"` VideoURL *string `json:"video_url"` Thumbnail *string `json:"thumbnail"` Duration *time.Duration `json:"duration"` Quality *education.VideoQuality `json:"quality"` IsPublished *bool `json:"is_published"` Tags []string `json:"tags"` } type AddLessonRequest struct { Title string `json:"title" binding:"required"` Description string `json:"description" binding:"required"` Content string `json:"content" binding:"required"` VideoURL string `json:"video_url"` Duration time.Duration `json:"duration" binding:"required"` Order int `json:"order" binding:"required"` IsFree bool `json:"is_free"` } type AddExerciseRequest struct { Title string `json:"title" binding:"required"` Description string `json:"description" binding:"required"` Content string `json:"content" binding:"required"` Solution string `json:"solution" binding:"required"` Type education.ExerciseType `json:"type" binding:"required"` Points int `json:"points" binding:"required"` TimeLimit time.Duration `json:"time_limit"` IsRequired bool `json:"is_required"` } type UpdateProgressRequest struct { Progress float64 `json:"progress" binding:"required"` CompletedLessons []string `json:"completed_lessons"` CurrentLesson string `json:"current_lesson"` Score float64 `json:"score"` TimeSpent time.Duration `json:"time_spent"` } type AddTutorialStepRequest struct { Title string `json:"title" binding:"required"` Description string `json:"description" binding:"required"` Content string `json:"content" binding:"required"` Order int `json:"order" binding:"required"` Timestamp time.Duration `json:"timestamp"` IsFree bool `json:"is_free"` } type AddTutorialCommentRequest struct { Content string `json:"content" binding:"required"` Rating int `json:"rating" binding:"min=1,max=5"` } // COURSES HANDLERS // CreateCourse crée un nouveau cours func (h *Handler) CreateCourse(c *gin.Context) { _, exists := common.GetUserIDFromContext(c) if !exists { response.Error(c, http.StatusUnauthorized, "Utilisateur non authentifié") return } var req CreateCourseRequest if err := c.ShouldBindJSON(&req); err != nil { response.Error(c, http.StatusBadRequest, "Données de requête invalides") return } course, err := h.courseManager.CreateCourse( c.Request.Context(), req.Title, req.Description, req.Instructor, req.Category, req.Level, req.Duration, req.Price, req.Language, ) if err != nil { h.logger.Error("Échec de création du cours", zap.Error(err)) response.Error(c, http.StatusInternalServerError, "Échec de création du cours") return } response.Success(c, course, "Cours créé avec succès") } // GetCourse récupère un cours par son ID func (h *Handler) GetCourse(c *gin.Context) { courseID := c.Param("course_id") if courseID == "" { response.Error(c, http.StatusBadRequest, "ID de cours requis") return } course, err := h.courseManager.GetCourse(c.Request.Context(), courseID) if err != nil { h.logger.Error("Échec de récupération du cours", zap.Error(err)) response.Error(c, http.StatusNotFound, "Cours non trouvé") return } response.Success(c, course, "Cours récupéré avec succès") } // ListCourses liste tous les cours disponibles func (h *Handler) ListCourses(c *gin.Context) { filters := make(map[string]interface{}) if category := c.Query("category"); category != "" { filters["category"] = category } if level := c.Query("level"); level != "" { filters["level"] = education.CourseLevel(level) } if isPublished := c.Query("is_published"); isPublished != "" { if published, err := strconv.ParseBool(isPublished); err == nil { filters["is_published"] = published } } if isFree := c.Query("is_free"); isFree != "" { if free, err := strconv.ParseBool(isFree); err == nil { filters["is_free"] = free } } courses, err := h.courseManager.ListCourses(c.Request.Context(), filters) if err != nil { h.logger.Error("Échec de récupération des cours", zap.Error(err)) response.Error(c, http.StatusInternalServerError, "Échec de récupération des cours") return } response.Success(c, courses, "Cours récupérés avec succès") } // UpdateCourse met à jour un cours func (h *Handler) UpdateCourse(c *gin.Context) { _, exists := common.GetUserIDFromContext(c) if !exists { response.Error(c, http.StatusUnauthorized, "Utilisateur non authentifié") return } courseID := c.Param("course_id") if courseID == "" { response.Error(c, http.StatusBadRequest, "ID de cours requis") return } var req UpdateCourseRequest if err := c.ShouldBindJSON(&req); err != nil { response.Error(c, http.StatusBadRequest, "Données de requête invalides") return } updates := make(map[string]interface{}) if req.Title != nil { updates["title"] = *req.Title } if req.Description != nil { updates["description"] = *req.Description } if req.Instructor != nil { updates["instructor"] = *req.Instructor } if req.Category != nil { updates["category"] = *req.Category } if req.Level != nil { updates["level"] = *req.Level } if req.Duration != nil { updates["duration"] = *req.Duration } if req.Price != nil { updates["price"] = *req.Price } if req.Language != nil { updates["language"] = *req.Language } if req.IsPublished != nil { updates["is_published"] = *req.IsPublished } if req.Tags != nil { updates["tags"] = req.Tags } course, err := h.courseManager.UpdateCourse(c.Request.Context(), courseID, updates) if err != nil { h.logger.Error("Échec de mise à jour du cours", zap.Error(err)) response.Error(c, http.StatusInternalServerError, "Échec de mise à jour du cours") return } response.Success(c, course, "Cours mis à jour avec succès") } // DeleteCourse supprime un cours func (h *Handler) DeleteCourse(c *gin.Context) { _, exists := common.GetUserIDFromContext(c) if !exists { response.Error(c, http.StatusUnauthorized, "Utilisateur non authentifié") return } courseID := c.Param("course_id") if courseID == "" { response.Error(c, http.StatusBadRequest, "ID de cours requis") return } err := h.courseManager.DeleteCourse(c.Request.Context(), courseID) if err != nil { h.logger.Error("Échec de suppression du cours", zap.Error(err)) response.Error(c, http.StatusInternalServerError, "Échec de suppression du cours") return } response.Success(c, nil, "Cours supprimé avec succès") } // AddLesson ajoute une leçon à un cours func (h *Handler) AddLesson(c *gin.Context) { _, exists := common.GetUserIDFromContext(c) if !exists { response.Error(c, http.StatusUnauthorized, "Utilisateur non authentifié") return } courseID := c.Param("course_id") if courseID == "" { response.Error(c, http.StatusBadRequest, "ID de cours requis") return } var req AddLessonRequest if err := c.ShouldBindJSON(&req); err != nil { response.Error(c, http.StatusBadRequest, "Données de requête invalides") return } lesson, err := h.courseManager.AddLesson( c.Request.Context(), courseID, req.Title, req.Description, req.Content, req.VideoURL, req.Duration, req.Order, req.IsFree, ) if err != nil { h.logger.Error("Échec d'ajout de leçon", zap.Error(err)) response.Error(c, http.StatusInternalServerError, "Échec d'ajout de leçon") return } response.Success(c, lesson, "Leçon ajoutée avec succès") } // AddExercise ajoute un exercice à un cours func (h *Handler) AddExercise(c *gin.Context) { _, exists := common.GetUserIDFromContext(c) if !exists { response.Error(c, http.StatusUnauthorized, "Utilisateur non authentifié") return } courseID := c.Param("course_id") lessonID := c.Param("lesson_id") if courseID == "" || lessonID == "" { response.Error(c, http.StatusBadRequest, "ID de cours et de leçon requis") return } var req AddExerciseRequest if err := c.ShouldBindJSON(&req); err != nil { response.Error(c, http.StatusBadRequest, "Données de requête invalides") return } exercise, err := h.courseManager.AddExercise( c.Request.Context(), courseID, lessonID, req.Title, req.Description, req.Content, req.Solution, req.Type, req.Points, req.TimeLimit, req.IsRequired, ) if err != nil { h.logger.Error("Échec d'ajout d'exercice", zap.Error(err)) response.Error(c, http.StatusInternalServerError, "Échec d'ajout d'exercice") return } response.Success(c, exercise, "Exercice ajouté avec succès") } // GetUserProgress récupère la progression d'un utilisateur func (h *Handler) GetUserProgress(c *gin.Context) { userID, exists := common.GetUserIDFromContext(c) if !exists { response.Error(c, http.StatusUnauthorized, "Utilisateur non authentifié") return } courseID := c.Param("course_id") if courseID == "" { response.Error(c, http.StatusBadRequest, "ID de cours requis") return } progress, err := h.courseManager.GetUserProgress(c.Request.Context(), userID, courseID) if err != nil { h.logger.Error("Échec de récupération de la progression", zap.Error(err)) response.Error(c, http.StatusNotFound, "Progression non trouvée") return } response.Success(c, progress, "Progression récupérée avec succès") } // UpdateUserProgress met à jour la progression d'un utilisateur func (h *Handler) UpdateUserProgress(c *gin.Context) { userID, exists := common.GetUserIDFromContext(c) if !exists { response.Error(c, http.StatusUnauthorized, "Utilisateur non authentifié") return } courseID := c.Param("course_id") if courseID == "" { response.Error(c, http.StatusBadRequest, "ID de cours requis") return } var req UpdateProgressRequest if err := c.ShouldBindJSON(&req); err != nil { response.Error(c, http.StatusBadRequest, "Données de requête invalides") return } progress, err := h.courseManager.UpdateUserProgress( c.Request.Context(), userID, courseID, req.Progress, req.CompletedLessons, req.CurrentLesson, req.Score, req.TimeSpent, ) if err != nil { h.logger.Error("Échec de mise à jour de la progression", zap.Error(err)) response.Error(c, http.StatusInternalServerError, "Échec de mise à jour de la progression") return } response.Success(c, progress, "Progression mise à jour avec succès") } // IssueCertificate émet un certificat func (h *Handler) IssueCertificate(c *gin.Context) { userID, exists := common.GetUserIDFromContext(c) if !exists { response.Error(c, http.StatusUnauthorized, "Utilisateur non authentifié") return } courseID := c.Param("course_id") if courseID == "" { response.Error(c, http.StatusBadRequest, "ID de cours requis") return } // Récupérer les paramètres de la requête title := c.Query("title") description := c.Query("description") scoreStr := c.Query("score") maxScoreStr := c.Query("max_score") if title == "" || description == "" || scoreStr == "" || maxScoreStr == "" { response.Error(c, http.StatusBadRequest, "Tous les paramètres sont requis") return } score, err := strconv.ParseFloat(scoreStr, 64) if err != nil { response.Error(c, http.StatusBadRequest, "Score invalide") return } maxScore, err := strconv.ParseFloat(maxScoreStr, 64) if err != nil { response.Error(c, http.StatusBadRequest, "Score maximum invalide") return } certificate, err := h.courseManager.IssueCertificate( c.Request.Context(), courseID, userID, title, description, score, maxScore, ) if err != nil { h.logger.Error("Échec d'émission du certificat", zap.Error(err)) response.Error(c, http.StatusInternalServerError, "Échec d'émission du certificat") return } response.Success(c, certificate, "Certificat émis avec succès") } // TUTORIALS HANDLERS // CreateTutorial crée un nouveau tutoriel func (h *Handler) CreateTutorial(c *gin.Context) { _, exists := common.GetUserIDFromContext(c) if !exists { response.Error(c, http.StatusUnauthorized, "Utilisateur non authentifié") return } var req CreateTutorialRequest if err := c.ShouldBindJSON(&req); err != nil { response.Error(c, http.StatusBadRequest, "Données de requête invalides") return } tutorial, err := h.tutorialManager.CreateTutorial( c.Request.Context(), req.Title, req.Description, req.Author, req.Category, req.VideoURL, req.Thumbnail, req.Language, req.Duration, req.Quality, req.IsFree, req.Tags, ) if err != nil { h.logger.Error("Échec de création du tutoriel", zap.Error(err)) response.Error(c, http.StatusInternalServerError, "Échec de création du tutoriel") return } response.Success(c, tutorial, "Tutoriel créé avec succès") } // GetTutorial récupère un tutoriel par son ID func (h *Handler) GetTutorial(c *gin.Context) { tutorialID := c.Param("tutorial_id") if tutorialID == "" { response.Error(c, http.StatusBadRequest, "ID de tutoriel requis") return } tutorial, err := h.tutorialManager.GetTutorial(c.Request.Context(), tutorialID) if err != nil { h.logger.Error("Échec de récupération du tutoriel", zap.Error(err)) response.Error(c, http.StatusNotFound, "Tutoriel non trouvé") return } // Incrémenter les vues go func() { if err := h.tutorialManager.IncrementViews(c.Request.Context(), tutorialID); err != nil { h.logger.Error("Échec d'incrémentation des vues", zap.Error(err)) } }() response.Success(c, tutorial, "Tutoriel récupéré avec succès") } // ListTutorials liste tous les tutoriels disponibles func (h *Handler) ListTutorials(c *gin.Context) { filters := make(map[string]interface{}) if category := c.Query("category"); category != "" { filters["category"] = category } if isPublished := c.Query("is_published"); isPublished != "" { if published, err := strconv.ParseBool(isPublished); err == nil { filters["is_published"] = published } } if isFree := c.Query("is_free"); isFree != "" { if free, err := strconv.ParseBool(isFree); err == nil { filters["is_free"] = free } } if language := c.Query("language"); language != "" { filters["language"] = language } if author := c.Query("author"); author != "" { filters["author"] = author } tutorials, err := h.tutorialManager.ListTutorials(c.Request.Context(), filters) if err != nil { h.logger.Error("Échec de récupération des tutoriels", zap.Error(err)) response.Error(c, http.StatusInternalServerError, "Échec de récupération des tutoriels") return } response.Success(c, tutorials, "Tutoriels récupérés avec succès") } // SearchTutorials recherche des tutoriels func (h *Handler) SearchTutorials(c *gin.Context) { query := c.Query("q") if query == "" { response.Error(c, http.StatusBadRequest, "Terme de recherche requis") return } filters := make(map[string]interface{}) if category := c.Query("category"); category != "" { filters["category"] = category } if isPublished := c.Query("is_published"); isPublished != "" { if published, err := strconv.ParseBool(isPublished); err == nil { filters["is_published"] = published } } if isFree := c.Query("is_free"); isFree != "" { if free, err := strconv.ParseBool(isFree); err == nil { filters["is_free"] = free } } tutorials, err := h.tutorialManager.SearchTutorials(c.Request.Context(), query, filters) if err != nil { h.logger.Error("Échec de recherche des tutoriels", zap.Error(err)) response.Error(c, http.StatusInternalServerError, "Échec de recherche des tutoriels") return } response.Success(c, tutorials, "Recherche de tutoriels terminée") } // UpdateTutorial met à jour un tutoriel func (h *Handler) UpdateTutorial(c *gin.Context) { _, exists := common.GetUserIDFromContext(c) if !exists { response.Error(c, http.StatusUnauthorized, "Utilisateur non authentifié") return } tutorialID := c.Param("tutorial_id") if tutorialID == "" { response.Error(c, http.StatusBadRequest, "ID de tutoriel requis") return } var req UpdateTutorialRequest if err := c.ShouldBindJSON(&req); err != nil { response.Error(c, http.StatusBadRequest, "Données de requête invalides") return } updates := make(map[string]interface{}) if req.Title != nil { updates["title"] = *req.Title } if req.Description != nil { updates["description"] = *req.Description } if req.Author != nil { updates["author"] = *req.Author } if req.Category != nil { updates["category"] = *req.Category } if req.VideoURL != nil { updates["video_url"] = *req.VideoURL } if req.Thumbnail != nil { updates["thumbnail"] = *req.Thumbnail } if req.Duration != nil { updates["duration"] = *req.Duration } if req.Quality != nil { updates["quality"] = *req.Quality } if req.IsPublished != nil { updates["is_published"] = *req.IsPublished } if req.Tags != nil { updates["tags"] = req.Tags } tutorial, err := h.tutorialManager.UpdateTutorial(c.Request.Context(), tutorialID, updates) if err != nil { h.logger.Error("Échec de mise à jour du tutoriel", zap.Error(err)) response.Error(c, http.StatusInternalServerError, "Échec de mise à jour du tutoriel") return } response.Success(c, tutorial, "Tutoriel mis à jour avec succès") } // DeleteTutorial supprime un tutoriel func (h *Handler) DeleteTutorial(c *gin.Context) { _, exists := common.GetUserIDFromContext(c) if !exists { response.Error(c, http.StatusUnauthorized, "Utilisateur non authentifié") return } tutorialID := c.Param("tutorial_id") if tutorialID == "" { response.Error(c, http.StatusBadRequest, "ID de tutoriel requis") return } err := h.tutorialManager.DeleteTutorial(c.Request.Context(), tutorialID) if err != nil { h.logger.Error("Échec de suppression du tutoriel", zap.Error(err)) response.Error(c, http.StatusInternalServerError, "Échec de suppression du tutoriel") return } response.Success(c, nil, "Tutoriel supprimé avec succès") } // AddTutorialStep ajoute une étape à un tutoriel func (h *Handler) AddTutorialStep(c *gin.Context) { _, exists := common.GetUserIDFromContext(c) if !exists { response.Error(c, http.StatusUnauthorized, "Utilisateur non authentifié") return } tutorialID := c.Param("tutorial_id") if tutorialID == "" { response.Error(c, http.StatusBadRequest, "ID de tutoriel requis") return } var req AddTutorialStepRequest if err := c.ShouldBindJSON(&req); err != nil { response.Error(c, http.StatusBadRequest, "Données de requête invalides") return } step, err := h.tutorialManager.AddTutorialStep( c.Request.Context(), tutorialID, req.Title, req.Description, req.Content, req.Order, req.Timestamp, req.IsFree, ) if err != nil { h.logger.Error("Échec d'ajout d'étape de tutoriel", zap.Error(err)) response.Error(c, http.StatusInternalServerError, "Échec d'ajout d'étape de tutoriel") return } response.Success(c, step, "Étape de tutoriel ajoutée avec succès") } // GetTutorialSteps récupère les étapes d'un tutoriel func (h *Handler) GetTutorialSteps(c *gin.Context) { tutorialID := c.Param("tutorial_id") if tutorialID == "" { response.Error(c, http.StatusBadRequest, "ID de tutoriel requis") return } steps, err := h.tutorialManager.GetTutorialSteps(c.Request.Context(), tutorialID) if err != nil { h.logger.Error("Échec de récupération des étapes", zap.Error(err)) response.Error(c, http.StatusInternalServerError, "Échec de récupération des étapes") return } response.Success(c, steps, "Étapes récupérées avec succès") } // AddTutorialComment ajoute un commentaire à un tutoriel func (h *Handler) AddTutorialComment(c *gin.Context) { userID, exists := common.GetUserIDFromContext(c) if !exists { response.Error(c, http.StatusUnauthorized, "Utilisateur non authentifié") return } tutorialID := c.Param("tutorial_id") if tutorialID == "" { response.Error(c, http.StatusBadRequest, "ID de tutoriel requis") return } var req AddTutorialCommentRequest if err := c.ShouldBindJSON(&req); err != nil { response.Error(c, http.StatusBadRequest, "Données de requête invalides") return } username, _ := common.GetUsernameFromContext(c) if username == "" { username = "Utilisateur anonyme" } comment, err := h.tutorialManager.AddTutorialComment( c.Request.Context(), tutorialID, userID.String(), username, req.Content, req.Rating, ) if err != nil { h.logger.Error("Échec d'ajout de commentaire", zap.Error(err)) response.Error(c, http.StatusInternalServerError, "Échec d'ajout de commentaire") return } response.Success(c, comment, "Commentaire ajouté avec succès") } // GetTutorialComments récupère les commentaires d'un tutoriel func (h *Handler) GetTutorialComments(c *gin.Context) { tutorialID := c.Param("tutorial_id") if tutorialID == "" { response.Error(c, http.StatusBadRequest, "ID de tutoriel requis") return } comments, err := h.tutorialManager.GetTutorialComments(c.Request.Context(), tutorialID) if err != nil { h.logger.Error("Échec de récupération des commentaires", zap.Error(err)) response.Error(c, http.StatusInternalServerError, "Échec de récupération des commentaires") return } response.Success(c, comments, "Commentaires récupérés avec succès") } // LikeTutorial ajoute un like à un tutoriel func (h *Handler) LikeTutorial(c *gin.Context) { tutorialID := c.Param("tutorial_id") if tutorialID == "" { response.Error(c, http.StatusBadRequest, "ID de tutoriel requis") return } err := h.tutorialManager.LikeTutorial(c.Request.Context(), tutorialID) if err != nil { h.logger.Error("Échec d'ajout de like", zap.Error(err)) response.Error(c, http.StatusInternalServerError, "Échec d'ajout de like") return } response.Success(c, nil, "Like ajouté avec succès") } // DislikeTutorial ajoute un dislike à un tutoriel func (h *Handler) DislikeTutorial(c *gin.Context) { tutorialID := c.Param("tutorial_id") if tutorialID == "" { response.Error(c, http.StatusBadRequest, "ID de tutoriel requis") return } err := h.tutorialManager.DislikeTutorial(c.Request.Context(), tutorialID) if err != nil { h.logger.Error("Échec d'ajout de dislike", zap.Error(err)) response.Error(c, http.StatusInternalServerError, "Échec d'ajout de dislike") return } response.Success(c, nil, "Dislike ajouté avec succès") }