Compare commits
No commits in common. "2ff1e89263d7e2c4f6760e128d01f21fcdc62509" and "894e18bcaed3d7dfa4af93342bc8396a1b645d56" have entirely different histories.
2ff1e89263
...
894e18bcae
|
|
@ -175,7 +175,7 @@ VALUES
|
||||||
'Administrative staff managing day-to-day operations.',
|
'Administrative staff managing day-to-day operations.',
|
||||||
'active',
|
'active',
|
||||||
TRUE,
|
TRUE,
|
||||||
'["*"]'::jsonb,
|
'[*]'::jsonb,
|
||||||
CURRENT_TIMESTAMP
|
CURRENT_TIMESTAMP
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
|
|
@ -292,15 +292,3 @@ ON CONFLICT (id) DO NOTHING;
|
||||||
UPDATE team_members
|
UPDATE team_members
|
||||||
SET permissions = '["*"]'::jsonb
|
SET permissions = '["*"]'::jsonb
|
||||||
WHERE id = 2 OR email = 'admin@yimaru.com';
|
WHERE id = 2 OR email = 'admin@yimaru.com';
|
||||||
|
|
||||||
-- ======================================================
|
|
||||||
-- RBAC safety seed: ensure ADMIN has permission grants
|
|
||||||
-- NOTE: API authorization uses RBAC role_permissions, not
|
|
||||||
-- team_members.permissions JSON.
|
|
||||||
-- ======================================================
|
|
||||||
INSERT INTO role_permissions (role_id, permission_id)
|
|
||||||
SELECT r.id, p.id
|
|
||||||
FROM roles r
|
|
||||||
CROSS JOIN permissions p
|
|
||||||
WHERE r.name = 'ADMIN'
|
|
||||||
ON CONFLICT (role_id, permission_id) DO NOTHING;
|
|
||||||
|
|
@ -1,113 +0,0 @@
|
||||||
package dbgen
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (q *Queries) GetSubModuleByIDCompat(ctx context.Context, id int64) (SubModule, error) {
|
|
||||||
row := q.db.QueryRow(ctx, `
|
|
||||||
SELECT id, module_id, title, description, display_order, is_active, created_at, legacy_sub_course_id
|
|
||||||
FROM sub_modules
|
|
||||||
WHERE id = $1
|
|
||||||
`, id)
|
|
||||||
var i SubModule
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.ModuleID,
|
|
||||||
&i.Title,
|
|
||||||
&i.Description,
|
|
||||||
&i.DisplayOrder,
|
|
||||||
&i.IsActive,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.LegacySubCourseID,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) UpdateSubModuleCompat(ctx context.Context, id int64, title string, description string, isActive bool) error {
|
|
||||||
_, err := q.db.Exec(ctx, `
|
|
||||||
UPDATE sub_modules
|
|
||||||
SET
|
|
||||||
title = $1,
|
|
||||||
description = NULLIF($2, ''),
|
|
||||||
is_active = $3
|
|
||||||
WHERE id = $4
|
|
||||||
`, title, description, isActive, id)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) DeleteSubModuleCompat(ctx context.Context, id int64) error {
|
|
||||||
_, err := q.db.Exec(ctx, `DELETE FROM sub_modules WHERE id = $1`, id)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) UpdateSubModuleVideoCompat(ctx context.Context, id int64, title string, description string, videoURL string) error {
|
|
||||||
_, err := q.db.Exec(ctx, `
|
|
||||||
UPDATE sub_module_videos
|
|
||||||
SET
|
|
||||||
title = $1,
|
|
||||||
description = NULLIF($2, ''),
|
|
||||||
video_url = $3
|
|
||||||
WHERE id = $4
|
|
||||||
`, title, description, videoURL, id)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) DeleteSubModuleVideoCompat(ctx context.Context, id int64) error {
|
|
||||||
_, err := q.db.Exec(ctx, `DELETE FROM sub_module_videos WHERE id = $1`, id)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) UpdatePracticeCompat(ctx context.Context, id int64, title string, description string, persona string) error {
|
|
||||||
_, err := q.db.Exec(ctx, `
|
|
||||||
UPDATE question_sets
|
|
||||||
SET
|
|
||||||
title = $1,
|
|
||||||
description = NULLIF($2, ''),
|
|
||||||
persona = NULLIF($3, ''),
|
|
||||||
updated_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE id = $4
|
|
||||||
`, title, description, persona, id)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = q.db.Exec(ctx, `
|
|
||||||
UPDATE sub_module_practices
|
|
||||||
SET
|
|
||||||
title = $1,
|
|
||||||
description = NULLIF($2, '')
|
|
||||||
WHERE question_set_id = $3
|
|
||||||
`, title, description, id)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) UpdatePracticeStatusCompat(ctx context.Context, id int64, isActive bool) error {
|
|
||||||
status := "ARCHIVED"
|
|
||||||
if isActive {
|
|
||||||
status = "PUBLISHED"
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := q.db.Exec(ctx, `
|
|
||||||
UPDATE question_sets
|
|
||||||
SET
|
|
||||||
status = $1,
|
|
||||||
updated_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE id = $2
|
|
||||||
`, status, id)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = q.db.Exec(ctx, `
|
|
||||||
UPDATE sub_module_practices
|
|
||||||
SET is_active = $1
|
|
||||||
WHERE question_set_id = $2
|
|
||||||
`, isActive, id)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) DeletePracticeCompat(ctx context.Context, id int64) error {
|
|
||||||
_, err := q.db.Exec(ctx, `DELETE FROM question_sets WHERE id = $1`, id)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
@ -30,10 +30,9 @@ func NewService(store ports.RBACStore, logger *slog.Logger) *Service {
|
||||||
}
|
}
|
||||||
|
|
||||||
// HasPermission checks if a role has a specific permission key.
|
// HasPermission checks if a role has a specific permission key.
|
||||||
// SUPER_ADMIN and ADMIN always return true to keep admin panel
|
// SUPER_ADMIN always returns true.
|
||||||
// access resilient even when RBAC seed data is partially missing.
|
|
||||||
func (s *Service) HasPermission(roleName, permKey string) bool {
|
func (s *Service) HasPermission(roleName, permKey string) bool {
|
||||||
if roleName == string(domain.RoleSuperAdmin) || roleName == string(domain.RoleAdmin) {
|
if roleName == string(domain.RoleSuperAdmin) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
snap := s.cache.Load().(*snapshot)
|
snap := s.cache.Load().(*snapshot)
|
||||||
|
|
|
||||||
|
|
@ -18,32 +18,6 @@ type createCourseSubCategoryReq struct {
|
||||||
IsActive *bool `json:"is_active"`
|
IsActive *bool `json:"is_active"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type createCourseCategoryReq struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
IsActive *bool `json:"is_active"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type createCourseReq struct {
|
|
||||||
CategoryID int64 `json:"category_id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Description *string `json:"description"`
|
|
||||||
Thumbnail *string `json:"thumbnail"`
|
|
||||||
IntroVideoURL *string `json:"intro_video_url"`
|
|
||||||
IsActive *bool `json:"is_active"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type updateCourseReq struct {
|
|
||||||
Title *string `json:"title"`
|
|
||||||
Description *string `json:"description"`
|
|
||||||
Thumbnail *string `json:"thumbnail"`
|
|
||||||
IntroVideoURL *string `json:"intro_video_url"`
|
|
||||||
IsActive *bool `json:"is_active"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type updateCourseThumbnailReq struct {
|
|
||||||
ThumbnailURL string `json:"thumbnail_url"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type createLevelReq struct {
|
type createLevelReq struct {
|
||||||
CourseID int64 `json:"course_id"`
|
CourseID int64 `json:"course_id"`
|
||||||
CEFRLevel string `json:"cefr_level"`
|
CEFRLevel string `json:"cefr_level"`
|
||||||
|
|
@ -81,25 +55,6 @@ type createSubModuleVideoReq struct {
|
||||||
Status *string `json:"status"`
|
Status *string `json:"status"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type updateSubModuleReq struct {
|
|
||||||
Title *string `json:"title"`
|
|
||||||
Description *string `json:"description"`
|
|
||||||
IsActive *bool `json:"is_active"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type updateSubModuleVideoReq struct {
|
|
||||||
Title *string `json:"title"`
|
|
||||||
Description *string `json:"description"`
|
|
||||||
VideoURL *string `json:"video_url"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type updatePracticeReq struct {
|
|
||||||
Title *string `json:"title"`
|
|
||||||
Description *string `json:"description"`
|
|
||||||
Persona *string `json:"persona"`
|
|
||||||
IsActive *bool `json:"is_active"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type attachSubModuleLessonReq struct {
|
type attachSubModuleLessonReq struct {
|
||||||
SubModuleID int64 `json:"sub_module_id"`
|
SubModuleID int64 `json:"sub_module_id"`
|
||||||
QuestionSetID int64 `json:"question_set_id"`
|
QuestionSetID int64 `json:"question_set_id"`
|
||||||
|
|
@ -119,15 +74,6 @@ type createSubModulePracticeReq struct {
|
||||||
IsActive *bool `json:"is_active"`
|
IsActive *bool `json:"is_active"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type legacyHierarchyRow struct {
|
|
||||||
CategoryID int64 `json:"category_id"`
|
|
||||||
CategoryName string `json:"category_name"`
|
|
||||||
SubCategoryID *int64 `json:"sub_category_id"`
|
|
||||||
SubCategoryName *string `json:"sub_category_name"`
|
|
||||||
CourseID *int64 `json:"course_id"`
|
|
||||||
CourseTitle *string `json:"course_title"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func toText(v *string) pgtype.Text {
|
func toText(v *string) pgtype.Text {
|
||||||
if v == nil {
|
if v == nil {
|
||||||
return pgtype.Text{Valid: false}
|
return pgtype.Text{Valid: false}
|
||||||
|
|
@ -156,245 +102,6 @@ func intOrNil(v *int32) interface{} {
|
||||||
return *v
|
return *v
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListCourseCategories godoc
|
|
||||||
// @Summary List course categories
|
|
||||||
// @Description Legacy-compatible endpoint for listing course categories
|
|
||||||
// @Tags course-management
|
|
||||||
// @Produce json
|
|
||||||
// @Success 200 {object} domain.Response
|
|
||||||
// @Failure 500 {object} domain.ErrorResponse
|
|
||||||
// @Router /api/v1/course-management/categories [get]
|
|
||||||
func (h *Handler) ListCourseCategories(c *fiber.Ctx) error {
|
|
||||||
rows, err := h.analyticsDB.GetAllCourseCategories(c.Context(), dbgen.GetAllCourseCategoriesParams{
|
|
||||||
Offset: pgtype.Int4{Int32: 0, Valid: true},
|
|
||||||
Limit: pgtype.Int4{Int32: 10000, Valid: true},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load categories", Error: err.Error()})
|
|
||||||
}
|
|
||||||
|
|
||||||
total := 0
|
|
||||||
if len(rows) > 0 {
|
|
||||||
total = int(rows[0].TotalCount)
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.JSON(domain.Response{
|
|
||||||
Message: "Categories retrieved successfully",
|
|
||||||
Data: map[string]interface{}{
|
|
||||||
"categories": rows,
|
|
||||||
"total_count": total,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateCourseCategory godoc
|
|
||||||
// @Summary Create course category
|
|
||||||
// @Description Legacy-compatible endpoint for creating a course category
|
|
||||||
// @Tags course-management
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Param body body createCourseCategoryReq true "Create category payload"
|
|
||||||
// @Success 201 {object} domain.Response
|
|
||||||
// @Failure 400 {object} domain.ErrorResponse
|
|
||||||
// @Failure 500 {object} domain.ErrorResponse
|
|
||||||
// @Router /api/v1/course-management/categories [post]
|
|
||||||
func (h *Handler) CreateCourseCategory(c *fiber.Ctx) error {
|
|
||||||
var req createCourseCategoryReq
|
|
||||||
if err := c.BodyParser(&req); err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()})
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(req.Name) == "" {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "name is required"})
|
|
||||||
}
|
|
||||||
|
|
||||||
created, err := h.analyticsDB.CreateCourseCategory(c.Context(), dbgen.CreateCourseCategoryParams{
|
|
||||||
Name: req.Name,
|
|
||||||
Column2: boolOrNil(req.IsActive),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to create category", Error: err.Error()})
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.Status(fiber.StatusCreated).JSON(domain.Response{Message: "Course category created", Data: created})
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateCourse godoc
|
|
||||||
// @Summary Create course
|
|
||||||
// @Description Legacy-compatible endpoint for creating a course
|
|
||||||
// @Tags course-management
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Param body body createCourseReq true "Create course payload"
|
|
||||||
// @Success 201 {object} domain.Response
|
|
||||||
// @Failure 400 {object} domain.ErrorResponse
|
|
||||||
// @Failure 500 {object} domain.ErrorResponse
|
|
||||||
// @Router /api/v1/course-management/courses [post]
|
|
||||||
func (h *Handler) CreateCourse(c *fiber.Ctx) error {
|
|
||||||
var req createCourseReq
|
|
||||||
if err := c.BodyParser(&req); err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()})
|
|
||||||
}
|
|
||||||
if req.CategoryID <= 0 || strings.TrimSpace(req.Title) == "" {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "category_id and title are required"})
|
|
||||||
}
|
|
||||||
|
|
||||||
created, err := h.analyticsDB.CreateCourse(c.Context(), dbgen.CreateCourseParams{
|
|
||||||
CategoryID: req.CategoryID,
|
|
||||||
Title: req.Title,
|
|
||||||
Description: toText(req.Description),
|
|
||||||
Thumbnail: toText(req.Thumbnail),
|
|
||||||
IntroVideoUrl: toText(req.IntroVideoURL),
|
|
||||||
Column6: boolOrNil(req.IsActive),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to create course", Error: err.Error()})
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.Status(fiber.StatusCreated).JSON(domain.Response{Message: "Course created", Data: created})
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateCourse godoc
|
|
||||||
// @Summary Update course
|
|
||||||
// @Description Legacy-compatible endpoint for updating a course
|
|
||||||
// @Tags course-management
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Param courseId path int true "Course ID"
|
|
||||||
// @Param body body updateCourseReq true "Update course payload"
|
|
||||||
// @Success 200 {object} domain.Response
|
|
||||||
// @Failure 400 {object} domain.ErrorResponse
|
|
||||||
// @Failure 500 {object} domain.ErrorResponse
|
|
||||||
// @Router /api/v1/course-management/courses/{courseId} [put]
|
|
||||||
func (h *Handler) UpdateCourse(c *fiber.Ctx) error {
|
|
||||||
courseID, err := strconv.ParseInt(c.Params("courseId"), 10, 64)
|
|
||||||
if err != nil || courseID <= 0 {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid course ID", Error: "courseId must be a positive integer"})
|
|
||||||
}
|
|
||||||
|
|
||||||
var req updateCourseReq
|
|
||||||
if err := c.BodyParser(&req); err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()})
|
|
||||||
}
|
|
||||||
|
|
||||||
existing, err := h.analyticsDB.GetCourseByID(c.Context(), courseID)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load course", Error: err.Error()})
|
|
||||||
}
|
|
||||||
|
|
||||||
title := existing.Title
|
|
||||||
if req.Title != nil {
|
|
||||||
title = *req.Title
|
|
||||||
}
|
|
||||||
description := existing.Description
|
|
||||||
if req.Description != nil {
|
|
||||||
description = toText(req.Description)
|
|
||||||
}
|
|
||||||
thumbnail := existing.Thumbnail
|
|
||||||
if req.Thumbnail != nil {
|
|
||||||
thumbnail = toText(req.Thumbnail)
|
|
||||||
}
|
|
||||||
introVideo := existing.IntroVideoUrl
|
|
||||||
if req.IntroVideoURL != nil {
|
|
||||||
introVideo = toText(req.IntroVideoURL)
|
|
||||||
}
|
|
||||||
isActive := existing.IsActive
|
|
||||||
if req.IsActive != nil {
|
|
||||||
isActive = *req.IsActive
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.analyticsDB.UpdateCourse(c.Context(), dbgen.UpdateCourseParams{
|
|
||||||
Title: title,
|
|
||||||
Description: description,
|
|
||||||
Thumbnail: thumbnail,
|
|
||||||
IntroVideoUrl: introVideo,
|
|
||||||
IsActive: isActive,
|
|
||||||
ID: courseID,
|
|
||||||
}); err != nil {
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to update course", Error: err.Error()})
|
|
||||||
}
|
|
||||||
|
|
||||||
updated, err := h.analyticsDB.GetCourseByID(c.Context(), courseID)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Course updated but failed to fetch latest record", Error: err.Error()})
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.JSON(domain.Response{Message: "Course updated", Data: updated})
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteCourse godoc
|
|
||||||
// @Summary Delete course
|
|
||||||
// @Description Legacy-compatible endpoint for deleting a course
|
|
||||||
// @Tags course-management
|
|
||||||
// @Produce json
|
|
||||||
// @Param courseId path int true "Course ID"
|
|
||||||
// @Success 200 {object} domain.Response
|
|
||||||
// @Failure 400 {object} domain.ErrorResponse
|
|
||||||
// @Failure 500 {object} domain.ErrorResponse
|
|
||||||
// @Router /api/v1/course-management/courses/{courseId} [delete]
|
|
||||||
func (h *Handler) DeleteCourse(c *fiber.Ctx) error {
|
|
||||||
courseID, err := strconv.ParseInt(c.Params("courseId"), 10, 64)
|
|
||||||
if err != nil || courseID <= 0 {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid course ID", Error: "courseId must be a positive integer"})
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.analyticsDB.DeleteCourse(c.Context(), courseID); err != nil {
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to delete course", Error: err.Error()})
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.JSON(domain.Response{Message: "Course deleted"})
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateCourseThumbnail godoc
|
|
||||||
// @Summary Update course thumbnail
|
|
||||||
// @Description Legacy-compatible endpoint for updating course thumbnail
|
|
||||||
// @Tags course-management
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Param courseId path int true "Course ID"
|
|
||||||
// @Param body body updateCourseThumbnailReq true "Update course thumbnail payload"
|
|
||||||
// @Success 200 {object} domain.Response
|
|
||||||
// @Failure 400 {object} domain.ErrorResponse
|
|
||||||
// @Failure 500 {object} domain.ErrorResponse
|
|
||||||
// @Router /api/v1/course-management/courses/{courseId}/thumbnail [post]
|
|
||||||
func (h *Handler) UpdateCourseThumbnail(c *fiber.Ctx) error {
|
|
||||||
courseID, err := strconv.ParseInt(c.Params("courseId"), 10, 64)
|
|
||||||
if err != nil || courseID <= 0 {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid course ID", Error: "courseId must be a positive integer"})
|
|
||||||
}
|
|
||||||
|
|
||||||
var req updateCourseThumbnailReq
|
|
||||||
if err := c.BodyParser(&req); err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()})
|
|
||||||
}
|
|
||||||
|
|
||||||
existing, err := h.analyticsDB.GetCourseByID(c.Context(), courseID)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load course", Error: err.Error()})
|
|
||||||
}
|
|
||||||
thumb := req.ThumbnailURL
|
|
||||||
if strings.TrimSpace(thumb) == "" {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "thumbnail_url is required"})
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.analyticsDB.UpdateCourse(c.Context(), dbgen.UpdateCourseParams{
|
|
||||||
Title: existing.Title,
|
|
||||||
Description: existing.Description,
|
|
||||||
Thumbnail: pgtype.Text{String: thumb, Valid: true},
|
|
||||||
IntroVideoUrl: existing.IntroVideoUrl,
|
|
||||||
IsActive: existing.IsActive,
|
|
||||||
ID: courseID,
|
|
||||||
}); err != nil {
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to update course thumbnail", Error: err.Error()})
|
|
||||||
}
|
|
||||||
|
|
||||||
updated, err := h.analyticsDB.GetCourseByID(c.Context(), courseID)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Thumbnail updated but failed to fetch latest record", Error: err.Error()})
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.JSON(domain.Response{Message: "Course thumbnail updated", Data: updated})
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnifiedHierarchy godoc
|
// UnifiedHierarchy godoc
|
||||||
// @Summary Get unified course hierarchy
|
// @Summary Get unified course hierarchy
|
||||||
// @Description Returns full hierarchy: category -> sub-category -> course
|
// @Description Returns full hierarchy: category -> sub-category -> course
|
||||||
|
|
@ -406,13 +113,6 @@ func (h *Handler) UpdateCourseThumbnail(c *fiber.Ctx) error {
|
||||||
func (h *Handler) UnifiedHierarchy(c *fiber.Ctx) error {
|
func (h *Handler) UnifiedHierarchy(c *fiber.Ctx) error {
|
||||||
rows, err := h.analyticsDB.GetCoursesWithHierarchy(c.Context())
|
rows, err := h.analyticsDB.GetCoursesWithHierarchy(c.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if isMissingCourseSubCategoryTableErr(err) {
|
|
||||||
legacyRows, legacyErr := h.buildLegacyHierarchyRows(c)
|
|
||||||
if legacyErr != nil {
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load hierarchy", Error: legacyErr.Error()})
|
|
||||||
}
|
|
||||||
return c.JSON(domain.Response{Message: "Unified hierarchy retrieved successfully", Data: legacyRows})
|
|
||||||
}
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load hierarchy", Error: err.Error()})
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load hierarchy", Error: err.Error()})
|
||||||
}
|
}
|
||||||
return c.JSON(domain.Response{Message: "Unified hierarchy retrieved successfully", Data: rows})
|
return c.JSON(domain.Response{Message: "Unified hierarchy retrieved successfully", Data: rows})
|
||||||
|
|
@ -435,88 +135,11 @@ func (h *Handler) UnifiedHierarchyByCourse(c *fiber.Ctx) error {
|
||||||
}
|
}
|
||||||
rows, err := h.analyticsDB.GetFullHierarchyByCourseID(c.Context(), courseID)
|
rows, err := h.analyticsDB.GetFullHierarchyByCourseID(c.Context(), courseID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if isMissingCourseSubCategoryTableErr(err) {
|
|
||||||
course, getCourseErr := h.analyticsDB.GetCourseByID(c.Context(), courseID)
|
|
||||||
if getCourseErr != nil {
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load course hierarchy", Error: getCourseErr.Error()})
|
|
||||||
}
|
|
||||||
return c.JSON(domain.Response{
|
|
||||||
Message: "Course hierarchy retrieved successfully",
|
|
||||||
Data: []map[string]interface{}{
|
|
||||||
{
|
|
||||||
"course_id": course.ID,
|
|
||||||
"course_title": course.Title,
|
|
||||||
"level_id": nil,
|
|
||||||
"cefr_level": nil,
|
|
||||||
"module_id": nil,
|
|
||||||
"module_title": nil,
|
|
||||||
"sub_module_id": nil,
|
|
||||||
"sub_module_title": nil,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load course hierarchy", Error: err.Error()})
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load course hierarchy", Error: err.Error()})
|
||||||
}
|
}
|
||||||
return c.JSON(domain.Response{Message: "Course hierarchy retrieved successfully", Data: rows})
|
return c.JSON(domain.Response{Message: "Course hierarchy retrieved successfully", Data: rows})
|
||||||
}
|
}
|
||||||
|
|
||||||
func isMissingCourseSubCategoryTableErr(err error) bool {
|
|
||||||
if err == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return strings.Contains(strings.ToLower(err.Error()), "relation \"course_sub_categories\" does not exist")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) buildLegacyHierarchyRows(c *fiber.Ctx) ([]legacyHierarchyRow, error) {
|
|
||||||
categories, err := h.analyticsDB.GetAllCourseCategories(c.Context(), dbgen.GetAllCourseCategoriesParams{
|
|
||||||
Offset: pgtype.Int4{Int32: 0, Valid: true},
|
|
||||||
Limit: pgtype.Int4{Int32: 10000, Valid: true},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
out := make([]legacyHierarchyRow, 0, len(categories))
|
|
||||||
for _, cat := range categories {
|
|
||||||
courses, courseErr := h.analyticsDB.GetCoursesByCategory(c.Context(), dbgen.GetCoursesByCategoryParams{
|
|
||||||
CategoryID: cat.ID,
|
|
||||||
Offset: pgtype.Int4{Int32: 0, Valid: true},
|
|
||||||
Limit: pgtype.Int4{Int32: 10000, Valid: true},
|
|
||||||
})
|
|
||||||
if courseErr != nil {
|
|
||||||
return nil, courseErr
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(courses) == 0 {
|
|
||||||
out = append(out, legacyHierarchyRow{
|
|
||||||
CategoryID: cat.ID,
|
|
||||||
CategoryName: cat.Name,
|
|
||||||
SubCategoryID: nil,
|
|
||||||
SubCategoryName: nil,
|
|
||||||
CourseID: nil,
|
|
||||||
CourseTitle: nil,
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, course := range courses {
|
|
||||||
courseID := course.ID
|
|
||||||
courseTitle := course.Title
|
|
||||||
out = append(out, legacyHierarchyRow{
|
|
||||||
CategoryID: cat.ID,
|
|
||||||
CategoryName: cat.Name,
|
|
||||||
SubCategoryID: nil,
|
|
||||||
SubCategoryName: nil,
|
|
||||||
CourseID: &courseID,
|
|
||||||
CourseTitle: &courseTitle,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateCourseSubCategory godoc
|
// CreateCourseSubCategory godoc
|
||||||
// @Summary Create course sub-category
|
// @Summary Create course sub-category
|
||||||
// @Description Creates a sub-category under a course category
|
// @Description Creates a sub-category under a course category
|
||||||
|
|
@ -757,167 +380,3 @@ func (h *Handler) CreateSubModulePractice(c *fiber.Ctx) error {
|
||||||
return c.Status(fiber.StatusCreated).JSON(domain.Response{Message: "Practice created", Data: created})
|
return c.Status(fiber.StatusCreated).JSON(domain.Response{Message: "Practice created", Data: created})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) GetSubModuleVideos(c *fiber.Ctx) error {
|
|
||||||
subModuleID, err := strconv.ParseInt(c.Params("subModuleId"), 10, 64)
|
|
||||||
if err != nil || subModuleID <= 0 {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid sub-module ID", Error: "subModuleId must be a positive integer"})
|
|
||||||
}
|
|
||||||
|
|
||||||
videos, err := h.analyticsDB.GetSubModuleVideos(c.Context(), subModuleID)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load sub-module videos", Error: err.Error()})
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.JSON(domain.Response{
|
|
||||||
Message: "Sub-module videos retrieved successfully",
|
|
||||||
Data: map[string]interface{}{
|
|
||||||
"videos": videos,
|
|
||||||
"total_count": len(videos),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) UpdateSubModule(c *fiber.Ctx) error {
|
|
||||||
subModuleID, err := strconv.ParseInt(c.Params("subModuleId"), 10, 64)
|
|
||||||
if err != nil || subModuleID <= 0 {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid sub-module ID", Error: "subModuleId must be a positive integer"})
|
|
||||||
}
|
|
||||||
|
|
||||||
existing, err := h.analyticsDB.GetSubModuleByIDCompat(c.Context(), subModuleID)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load sub-module", Error: err.Error()})
|
|
||||||
}
|
|
||||||
|
|
||||||
var req updateSubModuleReq
|
|
||||||
if err := c.BodyParser(&req); err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()})
|
|
||||||
}
|
|
||||||
|
|
||||||
title := existing.Title
|
|
||||||
if req.Title != nil {
|
|
||||||
title = *req.Title
|
|
||||||
}
|
|
||||||
description := ""
|
|
||||||
if existing.Description.Valid {
|
|
||||||
description = existing.Description.String
|
|
||||||
}
|
|
||||||
if req.Description != nil {
|
|
||||||
description = *req.Description
|
|
||||||
}
|
|
||||||
isActive := existing.IsActive
|
|
||||||
if req.IsActive != nil {
|
|
||||||
isActive = *req.IsActive
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.analyticsDB.UpdateSubModuleCompat(c.Context(), subModuleID, title, description, isActive); err != nil {
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to update sub-module", Error: err.Error()})
|
|
||||||
}
|
|
||||||
|
|
||||||
updated, err := h.analyticsDB.GetSubModuleByIDCompat(c.Context(), subModuleID)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Sub-module updated but failed to fetch latest record", Error: err.Error()})
|
|
||||||
}
|
|
||||||
return c.JSON(domain.Response{Message: "Sub-module updated", Data: updated})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) DeleteSubModule(c *fiber.Ctx) error {
|
|
||||||
subModuleID, err := strconv.ParseInt(c.Params("subModuleId"), 10, 64)
|
|
||||||
if err != nil || subModuleID <= 0 {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid sub-module ID", Error: "subModuleId must be a positive integer"})
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.analyticsDB.DeleteSubModuleCompat(c.Context(), subModuleID); err != nil {
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to delete sub-module", Error: err.Error()})
|
|
||||||
}
|
|
||||||
return c.JSON(domain.Response{Message: "Sub-module deleted"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) UpdateSubModuleVideo(c *fiber.Ctx) error {
|
|
||||||
videoID, err := strconv.ParseInt(c.Params("videoId"), 10, 64)
|
|
||||||
if err != nil || videoID <= 0 {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid video ID", Error: "videoId must be a positive integer"})
|
|
||||||
}
|
|
||||||
|
|
||||||
var req updateSubModuleVideoReq
|
|
||||||
if err := c.BodyParser(&req); err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()})
|
|
||||||
}
|
|
||||||
if req.Title == nil || strings.TrimSpace(*req.Title) == "" || req.VideoURL == nil || strings.TrimSpace(*req.VideoURL) == "" {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "title and video_url are required"})
|
|
||||||
}
|
|
||||||
|
|
||||||
description := ""
|
|
||||||
if req.Description != nil {
|
|
||||||
description = *req.Description
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.analyticsDB.UpdateSubModuleVideoCompat(c.Context(), videoID, *req.Title, description, *req.VideoURL); err != nil {
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to update sub-module video", Error: err.Error()})
|
|
||||||
}
|
|
||||||
return c.JSON(domain.Response{Message: "Sub-module video updated"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) DeleteSubModuleVideo(c *fiber.Ctx) error {
|
|
||||||
videoID, err := strconv.ParseInt(c.Params("videoId"), 10, 64)
|
|
||||||
if err != nil || videoID <= 0 {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid video ID", Error: "videoId must be a positive integer"})
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.analyticsDB.DeleteSubModuleVideoCompat(c.Context(), videoID); err != nil {
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to delete sub-module video", Error: err.Error()})
|
|
||||||
}
|
|
||||||
return c.JSON(domain.Response{Message: "Sub-module video deleted"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) UpdatePractice(c *fiber.Ctx) error {
|
|
||||||
practiceID, err := strconv.ParseInt(c.Params("practiceId"), 10, 64)
|
|
||||||
if err != nil || practiceID <= 0 {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid practice ID", Error: "practiceId must be a positive integer"})
|
|
||||||
}
|
|
||||||
|
|
||||||
var req updatePracticeReq
|
|
||||||
if err := c.BodyParser(&req); err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()})
|
|
||||||
}
|
|
||||||
|
|
||||||
if req.IsActive != nil {
|
|
||||||
if err := h.analyticsDB.UpdatePracticeStatusCompat(c.Context(), practiceID, *req.IsActive); err != nil {
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to update practice status", Error: err.Error()})
|
|
||||||
}
|
|
||||||
return c.JSON(domain.Response{Message: "Practice status updated"})
|
|
||||||
}
|
|
||||||
|
|
||||||
title := ""
|
|
||||||
if req.Title != nil {
|
|
||||||
title = *req.Title
|
|
||||||
}
|
|
||||||
description := ""
|
|
||||||
if req.Description != nil {
|
|
||||||
description = *req.Description
|
|
||||||
}
|
|
||||||
persona := ""
|
|
||||||
if req.Persona != nil {
|
|
||||||
persona = *req.Persona
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(title) == "" {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "title is required"})
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.analyticsDB.UpdatePracticeCompat(c.Context(), practiceID, title, description, persona); err != nil {
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to update practice", Error: err.Error()})
|
|
||||||
}
|
|
||||||
return c.JSON(domain.Response{Message: "Practice updated"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) DeletePractice(c *fiber.Ctx) error {
|
|
||||||
practiceID, err := strconv.ParseInt(c.Params("practiceId"), 10, 64)
|
|
||||||
if err != nil || practiceID <= 0 {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid practice ID", Error: "practiceId must be a positive integer"})
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.analyticsDB.DeletePracticeCompat(c.Context(), practiceID); err != nil {
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to delete practice", Error: err.Error()})
|
|
||||||
}
|
|
||||||
return c.JSON(domain.Response{Message: "Practice deleted"})
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -79,28 +79,15 @@ func (a *App) initAppRoutes() {
|
||||||
groupV1.Get("/assessment/questions/:id", h.GetAssessmentQuestionByID)
|
groupV1.Get("/assessment/questions/:id", h.GetAssessmentQuestionByID)
|
||||||
|
|
||||||
// Unified Course Management (single hierarchy model)
|
// Unified Course Management (single hierarchy model)
|
||||||
groupV1.Get("/course-management/categories", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.ListCourseCategories)
|
|
||||||
groupV1.Post("/course-management/categories", a.authMiddleware, a.RequirePermission("course_categories.create"), h.CreateCourseCategory)
|
|
||||||
groupV1.Post("/course-management/courses", a.authMiddleware, a.RequirePermission("courses.create"), h.CreateCourse)
|
|
||||||
groupV1.Put("/course-management/courses/:courseId", a.authMiddleware, a.RequirePermission("courses.update"), h.UpdateCourse)
|
|
||||||
groupV1.Delete("/course-management/courses/:courseId", a.authMiddleware, a.RequirePermission("courses.delete"), h.DeleteCourse)
|
|
||||||
groupV1.Post("/course-management/courses/:courseId/thumbnail", a.authMiddleware, a.RequirePermission("courses.upload_thumbnail"), h.UpdateCourseThumbnail)
|
|
||||||
groupV1.Get("/course-management/hierarchy", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.UnifiedHierarchy)
|
groupV1.Get("/course-management/hierarchy", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.UnifiedHierarchy)
|
||||||
groupV1.Get("/course-management/courses/:courseId/hierarchy", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.UnifiedHierarchyByCourse)
|
groupV1.Get("/course-management/courses/:courseId/hierarchy", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.UnifiedHierarchyByCourse)
|
||||||
groupV1.Post("/course-management/sub-categories", a.authMiddleware, a.RequirePermission("course_categories.create"), h.CreateCourseSubCategory)
|
groupV1.Post("/course-management/sub-categories", a.authMiddleware, a.RequirePermission("course_categories.create"), h.CreateCourseSubCategory)
|
||||||
groupV1.Post("/course-management/levels", a.authMiddleware, a.RequirePermission("subcourses.create"), h.CreateLevel)
|
groupV1.Post("/course-management/levels", a.authMiddleware, a.RequirePermission("subcourses.create"), h.CreateLevel)
|
||||||
groupV1.Post("/course-management/modules", a.authMiddleware, a.RequirePermission("subcourses.create"), h.CreateModule)
|
groupV1.Post("/course-management/modules", a.authMiddleware, a.RequirePermission("subcourses.create"), h.CreateModule)
|
||||||
groupV1.Post("/course-management/sub-modules", a.authMiddleware, a.RequirePermission("subcourses.create"), h.CreateSubModule)
|
groupV1.Post("/course-management/sub-modules", a.authMiddleware, a.RequirePermission("subcourses.create"), h.CreateSubModule)
|
||||||
groupV1.Put("/course-management/sub-modules/:subModuleId", a.authMiddleware, a.RequirePermission("subcourses.update"), h.UpdateSubModule)
|
|
||||||
groupV1.Delete("/course-management/sub-modules/:subModuleId", a.authMiddleware, a.RequirePermission("subcourses.delete"), h.DeleteSubModule)
|
|
||||||
groupV1.Get("/course-management/sub-modules/:subModuleId/videos", a.authMiddleware, a.RequirePermission("videos.list"), h.GetSubModuleVideos)
|
|
||||||
groupV1.Post("/course-management/sub-module-videos", a.authMiddleware, a.RequirePermission("videos.create"), h.CreateSubModuleVideo)
|
groupV1.Post("/course-management/sub-module-videos", a.authMiddleware, a.RequirePermission("videos.create"), h.CreateSubModuleVideo)
|
||||||
groupV1.Put("/course-management/sub-module-videos/:videoId", a.authMiddleware, a.RequirePermission("videos.update"), h.UpdateSubModuleVideo)
|
|
||||||
groupV1.Delete("/course-management/sub-module-videos/:videoId", a.authMiddleware, a.RequirePermission("videos.delete"), h.DeleteSubModuleVideo)
|
|
||||||
groupV1.Post("/course-management/sub-module-lessons", a.authMiddleware, a.RequirePermission("question_sets.update"), h.AttachSubModuleLesson)
|
groupV1.Post("/course-management/sub-module-lessons", a.authMiddleware, a.RequirePermission("question_sets.update"), h.AttachSubModuleLesson)
|
||||||
groupV1.Post("/course-management/sub-module-practices", a.authMiddleware, a.RequirePermission("question_sets.update"), h.CreateSubModulePractice)
|
groupV1.Post("/course-management/sub-module-practices", a.authMiddleware, a.RequirePermission("question_sets.update"), h.CreateSubModulePractice)
|
||||||
groupV1.Put("/course-management/practices/:practiceId", a.authMiddleware, a.RequirePermission("question_sets.update"), h.UpdatePractice)
|
|
||||||
groupV1.Delete("/course-management/practices/:practiceId", a.authMiddleware, a.RequirePermission("question_sets.delete"), h.DeletePractice)
|
|
||||||
|
|
||||||
// Questions
|
// Questions
|
||||||
groupV1.Post("/questions", a.authMiddleware, a.RequirePermission("questions.create"), h.CreateQuestion)
|
groupV1.Post("/questions", a.authMiddleware, a.RequirePermission("questions.create"), h.CreateQuestion)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user