Yimaru-BackEnd/internal/web_server/content_access_middleware.go

336 lines
10 KiB
Go

package httpserver
import (
"context"
"Yimaru-Backend/internal/domain"
lessonsvc "Yimaru-Backend/internal/services/lessons"
coursessvc "Yimaru-Backend/internal/services/courses"
modulesvc "Yimaru-Backend/internal/services/modules"
programssvc "Yimaru-Backend/internal/services/programs"
practicessvc "Yimaru-Backend/internal/services/practices"
"errors"
"fmt"
"strings"
"github.com/gofiber/fiber/v2"
"go.uber.org/zap"
)
func (a *App) RequireLMSSubscriptionUnlessFree() fiber.Handler {
return func(c *fiber.Ctx) error {
role, userID, err := subscriptionScopedUser(c)
if err != nil {
return err
}
if bypassSubscriptionForRole(role) {
return c.Next()
}
if role != domain.RoleStudent && role != domain.RoleOpenLearner {
return c.Next()
}
if domain.CategorySubscriptionGateDisabled {
return c.Next()
}
tier, resolved, err := a.resolveLMSEffectiveAccessTier(c)
if err != nil {
switch {
case errors.Is(err, programssvc.ErrProgramNotFound),
errors.Is(err, coursessvc.ErrCourseNotFound),
errors.Is(err, modulesvc.ErrModuleNotFound),
errors.Is(err, lessonsvc.ErrLessonNotFound),
errors.Is(err, practicessvc.ErrPracticeNotFound):
return fiber.NewError(fiber.StatusNotFound, err.Error())
default:
return fiber.NewError(fiber.StatusInternalServerError, "Failed to verify content access")
}
}
if !resolved || !tier.RequiresSubscription() {
return c.Next()
}
active, err := a.subscriptionsSvc.HasActiveSubscriptionByCategory(c.Context(), userID, domain.SubscriptionCategoryLearnEnglish)
if err != nil {
a.mongoLoggerSvc.Error("category subscription check failed",
zap.Int64("userID", userID),
zap.String("category", string(domain.SubscriptionCategoryLearnEnglish)),
zap.String("path", c.Path()),
zap.Error(err),
)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to verify subscription")
}
if !active {
return fiber.NewError(fiber.StatusForbidden, fmt.Sprintf("An active %s subscription is required", humanizeSubscriptionCategory(domain.SubscriptionCategoryLearnEnglish)))
}
return c.Next()
}
}
func (a *App) resolveLMSEffectiveAccessTier(c *fiber.Ctx) (domain.ContentAccessTier, bool, error) {
ctx := c.Context()
routePath := c.Route().Path
if strings.Contains(routePath, "/practices/:practiceId/questions") || strings.Contains(routePath, "/practices/:id") {
practiceID, ok, err := parseRouteInt64(c, "practiceId")
if err != nil {
return "", false, err
}
if !ok {
practiceID, ok, err = parseRouteInt64(c, "id")
if err != nil {
return "", false, err
}
if !ok {
goto unresolved
}
}
return a.lmsEffectiveTierForPractice(ctx, practiceID)
}
if lessonID, ok, err := parseRouteInt64(c, "lessonId"); err != nil {
return "", false, err
} else if ok {
return a.lmsEffectiveTierForLesson(ctx, lessonID)
}
if moduleID, ok, err := parseRouteInt64(c, "moduleId"); err != nil {
return "", false, err
} else if ok {
return a.lmsEffectiveTierForModule(ctx, moduleID)
}
if _, ok, err := parseRouteInt64(c, "courseId"); err != nil {
return "", false, err
} else if ok {
return "", false, nil
}
switch {
case strings.Contains(routePath, "/lessons/:id"):
if id, ok, err := parseRouteInt64(c, "id"); err != nil {
return "", false, err
} else if ok {
return a.lmsEffectiveTierForLesson(ctx, id)
}
case strings.Contains(routePath, "/modules/:id"):
if id, ok, err := parseRouteInt64(c, "id"); err != nil {
return "", false, err
} else if ok {
return a.lmsEffectiveTierForModule(ctx, id)
}
case strings.Contains(routePath, "/courses/:id"):
if id, ok, err := parseRouteInt64(c, "id"); err != nil {
return "", false, err
} else if ok {
return a.lmsEffectiveTierForCourse(ctx, id)
}
case strings.Contains(routePath, "/programs/:id"):
if id, ok, err := parseRouteInt64(c, "id"); err != nil {
return "", false, err
} else if ok {
return a.lmsEffectiveTierForProgram(ctx, id)
}
}
unresolved:
return "", false, nil
}
func (a *App) lmsEffectiveTierForProgram(ctx context.Context, programID int64) (domain.ContentAccessTier, bool, error) {
program, err := a.programSvc.GetByID(ctx, programID)
if err != nil {
return "", false, err
}
return program.AccessTier, true, nil
}
func (a *App) lmsEffectiveTierForCourse(ctx context.Context, courseID int64) (domain.ContentAccessTier, bool, error) {
course, err := a.courseSvc.GetByID(ctx, courseID)
if err != nil {
return "", false, err
}
program, err := a.programSvc.GetByID(ctx, course.ProgramID)
if err != nil {
return "", false, err
}
return domain.EffectiveContentAccessTier(program.AccessTier, course.AccessTier), true, nil
}
func (a *App) lmsEffectiveTierForModule(ctx context.Context, moduleID int64) (domain.ContentAccessTier, bool, error) {
module, err := a.moduleSvc.GetByID(ctx, moduleID)
if err != nil {
return "", false, err
}
course, err := a.courseSvc.GetByID(ctx, module.CourseID)
if err != nil {
return "", false, err
}
program, err := a.programSvc.GetByID(ctx, course.ProgramID)
if err != nil {
return "", false, err
}
return domain.EffectiveContentAccessTier(program.AccessTier, course.AccessTier, module.AccessTier), true, nil
}
func (a *App) lmsEffectiveTierForLesson(ctx context.Context, lessonID int64) (domain.ContentAccessTier, bool, error) {
lesson, err := a.lessonSvc.GetByID(ctx, lessonID)
if err != nil {
return "", false, err
}
module, err := a.moduleSvc.GetByID(ctx, lesson.ModuleID)
if err != nil {
return "", false, err
}
course, err := a.courseSvc.GetByID(ctx, module.CourseID)
if err != nil {
return "", false, err
}
program, err := a.programSvc.GetByID(ctx, course.ProgramID)
if err != nil {
return "", false, err
}
return domain.EffectiveContentAccessTier(program.AccessTier, course.AccessTier, module.AccessTier, lesson.AccessTier), true, nil
}
func (a *App) resolveExamPrepEffectiveAccessTier(c *fiber.Ctx) (domain.ContentAccessTier, bool, error) {
ctx := c.Context()
routePath := c.Route().Path
if _, ok, err := parseRouteInt64(c, "catalogCourseId"); err != nil {
return "", false, err
} else if ok {
return "", false, nil
}
if _, ok, err := parseRouteInt64(c, "unitId"); err != nil {
return "", false, err
} else if ok {
return "", false, nil
}
if _, ok, err := parseRouteInt64(c, "moduleId"); err != nil {
return "", false, err
} else if ok {
return "", false, nil
}
if _, ok, err := parseRouteInt64(c, "lessonId"); err != nil {
return "", false, err
} else if ok {
return "", false, nil
}
switch {
case strings.Contains(routePath, "/catalog-courses/:id"):
if id, ok, err := parseRouteInt64(c, "id"); err != nil {
return "", false, err
} else if ok {
return a.examPrepEffectiveTierForCatalogCourse(ctx, id)
}
case strings.Contains(routePath, "/units/:id"):
if id, ok, err := parseRouteInt64(c, "id"); err != nil {
return "", false, err
} else if ok {
return a.examPrepEffectiveTierForUnit(ctx, id)
}
case strings.Contains(routePath, "/modules/:id"):
if id, ok, err := parseRouteInt64(c, "id"); err != nil {
return "", false, err
} else if ok {
return a.examPrepEffectiveTierForModule(ctx, id)
}
case strings.Contains(routePath, "/lessons/:id"):
if id, ok, err := parseRouteInt64(c, "id"); err != nil {
return "", false, err
} else if ok {
return a.examPrepEffectiveTierForLesson(ctx, id)
}
case strings.Contains(routePath, "/practices/:id"):
if id, ok, err := parseRouteInt64(c, "id"); err != nil {
return "", false, err
} else if ok {
return a.examPrepEffectiveTierForPractice(ctx, id)
}
}
return "", false, nil
}
func (a *App) examPrepEffectiveTierForCatalogCourse(ctx context.Context, catalogCourseID int64) (domain.ContentAccessTier, bool, error) {
cc, err := a.examPrepSvc.GetCatalogCourseByID(ctx, catalogCourseID)
if err != nil {
return "", false, err
}
return cc.AccessTier, true, nil
}
func (a *App) examPrepEffectiveTierForUnit(ctx context.Context, unitID int64) (domain.ContentAccessTier, bool, error) {
unit, err := a.examPrepSvc.GetUnitByID(ctx, unitID)
if err != nil {
return "", false, err
}
cc, err := a.examPrepSvc.GetCatalogCourseByID(ctx, unit.CatalogCourseID)
if err != nil {
return "", false, err
}
return domain.EffectiveContentAccessTier(cc.AccessTier, unit.AccessTier), true, nil
}
func (a *App) examPrepEffectiveTierForModule(ctx context.Context, moduleID int64) (domain.ContentAccessTier, bool, error) {
module, err := a.examPrepSvc.GetModuleByID(ctx, moduleID)
if err != nil {
return "", false, err
}
unit, err := a.examPrepSvc.GetUnitByID(ctx, module.UnitID)
if err != nil {
return "", false, err
}
cc, err := a.examPrepSvc.GetCatalogCourseByID(ctx, unit.CatalogCourseID)
if err != nil {
return "", false, err
}
return domain.EffectiveContentAccessTier(cc.AccessTier, unit.AccessTier, module.AccessTier), true, nil
}
func (a *App) examPrepEffectiveTierForLesson(ctx context.Context, lessonID int64) (domain.ContentAccessTier, bool, error) {
lesson, err := a.examPrepSvc.GetLessonByID(ctx, lessonID)
if err != nil {
return "", false, err
}
module, err := a.examPrepSvc.GetModuleByID(ctx, lesson.UnitModuleID)
if err != nil {
return "", false, err
}
unit, err := a.examPrepSvc.GetUnitByID(ctx, module.UnitID)
if err != nil {
return "", false, err
}
cc, err := a.examPrepSvc.GetCatalogCourseByID(ctx, unit.CatalogCourseID)
if err != nil {
return "", false, err
}
return domain.EffectiveContentAccessTier(cc.AccessTier, unit.AccessTier, module.AccessTier, lesson.AccessTier), true, nil
}
func (a *App) examPrepEffectiveTierForPractice(ctx context.Context, practiceID int64) (domain.ContentAccessTier, bool, error) {
practice, err := a.examPrepSvc.GetExamPrepPracticeByID(ctx, practiceID)
if err != nil {
return "", false, err
}
return a.examPrepEffectiveTierForLesson(ctx, practice.LessonID)
}
func (a *App) lmsEffectiveTierForPractice(ctx context.Context, practiceID int64) (domain.ContentAccessTier, bool, error) {
practice, err := a.practiceSvc.GetByID(ctx, practiceID)
if err != nil {
return "", false, err
}
switch practice.ParentKind {
case domain.ParentKindLesson:
return a.lmsEffectiveTierForLesson(ctx, practice.ParentID)
case domain.ParentKindModule:
return a.lmsEffectiveTierForModule(ctx, practice.ParentID)
case domain.ParentKindCourse:
return a.lmsEffectiveTierForCourse(ctx, practice.ParentID)
default:
return "", false, nil
}
}