lifedebugger commited on
Commit
7d3a71d
·
1 Parent(s): f4187e3

Deploy files from GitHub repository

Browse files
controllers/academy_exam_controller.go CHANGED
@@ -14,15 +14,15 @@ type AcademyExamController interface {
14
  List(ctx *gin.Context)
15
  }
16
 
17
- type academyExamController struct { service services.AcademyExamService }
18
 
19
- func NewAcademyExamController(service services.AcademyExamService) AcademyExamController { return &academyExamController{ service: service } }
20
 
21
  func (c *academyExamController) Attempt(ctx *gin.Context) {
22
  academySlug := ctx.Param("academy_slug")
23
  examSlug := ctx.Param("exam_slug")
24
  accountId := ParseAccountId(ctx)
25
- res, err := c.service.AttemptExamAcademy(ctx.Request.Context(), academySlug, examSlug, accountId)
26
  ResponseJSON(ctx, gin.H{"academy_slug": academySlug, "exam_slug": examSlug}, res, err)
27
  }
28
 
@@ -30,19 +30,19 @@ func (c *academyExamController) Answer(ctx *gin.Context) {
30
  academySlug := ctx.Param("academy_slug")
31
  attemptId, _ := utils.ToUUID(ctx.Param("attempt_id"))
32
  req := RequestJSON[dto.AnswerExamEventRequest](ctx)
33
- res, err := c.service.AnswerExamAcademy(ctx.Request.Context(), academySlug, attemptId, req.QuestionId, req.Answer)
34
  ResponseJSON(ctx, gin.H{"cp_grader_result": res}, req, err)
35
  }
36
 
37
  func (c *academyExamController) Submit(ctx *gin.Context) {
38
  attemptId, _ := utils.ToUUID(ctx.Param("attempt_id"))
39
- res, err := c.service.SubmitExamAcademy(ctx.Request.Context(), attemptId)
40
  ResponseJSON(ctx, gin.H{}, res, err)
41
  }
42
 
43
  func (c *academyExamController) List(ctx *gin.Context) {
44
  academySlug := ctx.Param("academy_slug")
45
  accountId := ParseAccountId(ctx)
46
- res, err := c.service.ListExamByAcademy(ctx.Request.Context(), academySlug, accountId)
47
  ResponseJSON(ctx, gin.H{}, res, err)
48
  }
 
14
  List(ctx *gin.Context)
15
  }
16
 
17
+ type academyExamController struct { academyExamService services.AcademyExamService }
18
 
19
+ func NewAcademyExamController(academyExamService services.AcademyExamService) AcademyExamController { return &academyExamController{ academyExamService: academyExamService } }
20
 
21
  func (c *academyExamController) Attempt(ctx *gin.Context) {
22
  academySlug := ctx.Param("academy_slug")
23
  examSlug := ctx.Param("exam_slug")
24
  accountId := ParseAccountId(ctx)
25
+ res, err := c.academyExamService.AttemptExamAcademy(ctx.Request.Context(), academySlug, examSlug, accountId)
26
  ResponseJSON(ctx, gin.H{"academy_slug": academySlug, "exam_slug": examSlug}, res, err)
27
  }
28
 
 
30
  academySlug := ctx.Param("academy_slug")
31
  attemptId, _ := utils.ToUUID(ctx.Param("attempt_id"))
32
  req := RequestJSON[dto.AnswerExamEventRequest](ctx)
33
+ res, err := c.academyExamService.AnswerExamAcademy(ctx.Request.Context(), academySlug, attemptId, req.QuestionId, req.Answer)
34
  ResponseJSON(ctx, gin.H{"cp_grader_result": res}, req, err)
35
  }
36
 
37
  func (c *academyExamController) Submit(ctx *gin.Context) {
38
  attemptId, _ := utils.ToUUID(ctx.Param("attempt_id"))
39
+ res, err := c.academyExamService.SubmitExamAcademy(ctx.Request.Context(), attemptId)
40
  ResponseJSON(ctx, gin.H{}, res, err)
41
  }
42
 
43
  func (c *academyExamController) List(ctx *gin.Context) {
44
  academySlug := ctx.Param("academy_slug")
45
  accountId := ParseAccountId(ctx)
46
+ res, err := c.academyExamService.ListExamByAcademy(ctx.Request.Context(), academySlug, accountId)
47
  ResponseJSON(ctx, gin.H{}, res, err)
48
  }
controllers/upload_controller.go CHANGED
@@ -17,13 +17,16 @@ import (
17
  "abdanhafidz.com/go-boilerplate/services"
18
  )
19
 
20
- type UploadController struct {
21
- uploadService services.UploadService
 
22
  }
23
 
24
- func NewUploadController(s services.UploadService) *UploadController { return &UploadController{ uploadService: s } }
25
 
26
- func (c *UploadController) Upload(ctx *gin.Context) {
 
 
27
  fmt.Println("👉 Content-Type:", ctx.GetHeader("Content-Type"))
28
 
29
  if !strings.Contains(ctx.GetHeader("Content-Type"), "multipart/form-data") {
@@ -162,7 +165,7 @@ func (c *UploadController) Upload(ctx *gin.Context) {
162
  })
163
  }
164
 
165
- func (c *UploadController) GetFileByID(ctx *gin.Context) {
166
  fileIDStr := ctx.Param("id")
167
  fileID, err := uuid.Parse(fileIDStr)
168
  if err != nil {
@@ -224,7 +227,7 @@ func (c *UploadController) GetFileByID(ctx *gin.Context) {
224
  })
225
  }
226
 
227
- func (c *UploadController) inferContextFromExt(ext string) string {
228
  images := map[string]bool{
229
  ".jpg": true, ".jpeg": true, ".png": true, ".webp": true,
230
  }
 
17
  "abdanhafidz.com/go-boilerplate/services"
18
  )
19
 
20
+ type UploadController interface{
21
+ Upload(ctx *gin.Context)
22
+ GetFileByID(ctx *gin.Context)
23
  }
24
 
25
+ type uploadController struct{uploadService services.UploadService}
26
 
27
+ func NewUploadController(uploadService services.UploadService) UploadController { return &uploadController{ uploadService: uploadService } }
28
+
29
+ func (c *uploadController) Upload(ctx *gin.Context) {
30
  fmt.Println("👉 Content-Type:", ctx.GetHeader("Content-Type"))
31
 
32
  if !strings.Contains(ctx.GetHeader("Content-Type"), "multipart/form-data") {
 
165
  })
166
  }
167
 
168
+ func (c *uploadController) GetFileByID(ctx *gin.Context) {
169
  fileIDStr := ctx.Param("id")
170
  fileID, err := uuid.Parse(fileIDStr)
171
  if err != nil {
 
227
  })
228
  }
229
 
230
+ func (c *uploadController) inferContextFromExt(ext string) string {
231
  images := map[string]bool{
232
  ".jpg": true, ".jpeg": true, ".png": true, ".webp": true,
233
  }
models/dto/academy_dto.go CHANGED
@@ -5,6 +5,7 @@ import "github.com/google/uuid"
5
  type CreateAcademyRequest struct {
6
  Title string `json:"title" binding:"required"`
7
  Slug string `json:"slug"`
 
8
  Description string `json:"description"`
9
  ImageUrl string `json:"image_url"`
10
  }
@@ -65,6 +66,7 @@ type AcademyDetailResponse struct {
65
  Id uuid.UUID `json:"id"`
66
  Title string `json:"title"`
67
  Slug string `json:"slug"`
 
68
  Description string `json:"description"`
69
  ImageUrl string `json:"image_url"`
70
  MaterialsCount int64 `json:"materials_count"`
 
5
  type CreateAcademyRequest struct {
6
  Title string `json:"title" binding:"required"`
7
  Slug string `json:"slug"`
8
+ Code string `json:"code"`
9
  Description string `json:"description"`
10
  ImageUrl string `json:"image_url"`
11
  }
 
66
  Id uuid.UUID `json:"id"`
67
  Title string `json:"title"`
68
  Slug string `json:"slug"`
69
+ Code string `json:"code"`
70
  Description string `json:"description"`
71
  ImageUrl string `json:"image_url"`
72
  MaterialsCount int64 `json:"materials_count"`
models/entity/entity.go CHANGED
@@ -263,6 +263,7 @@ type Academy struct {
263
  Id uuid.UUID `gorm:"type:uuid;primaryKey" json:"id"`
264
  Title string `json:"title,omitempty"`
265
  Slug string `gorm:"unique" json:"slug,omitempty"`
 
266
  Description string `json:"description,omitempty"`
267
  ImageUrl string `json:"image_url,omitempty"`
268
  MaterialsCount int64 `json:"materials_count,omitempty"`
 
263
  Id uuid.UUID `gorm:"type:uuid;primaryKey" json:"id"`
264
  Title string `json:"title,omitempty"`
265
  Slug string `gorm:"unique" json:"slug,omitempty"`
266
+ Code string `gorm:"unique" json:"code,omitempty"`
267
  Description string `json:"description,omitempty"`
268
  ImageUrl string `json:"image_url,omitempty"`
269
  MaterialsCount int64 `json:"materials_count,omitempty"`
models/error/error.go CHANGED
@@ -30,6 +30,9 @@ var (
30
  EVENT_FINISHED = errors.New("The event has ended, you are disallowed to take the exam")
31
  EVENT_NOT_STARTED = errors.New("Take it easy, event hasn't started yet! You cannot take the exam")
32
  EXAMS_SUBMITTED = errors.New("You have submitted the exam, you are disallowed to answer the question")
 
 
 
33
 
34
  // ================= FILE UPLOAD =================
35
  FILE_TOO_LARGE = errors.New("File size exceeds the maximum limit")
 
30
  EVENT_FINISHED = errors.New("The event has ended, you are disallowed to take the exam")
31
  EVENT_NOT_STARTED = errors.New("Take it easy, event hasn't started yet! You cannot take the exam")
32
  EXAMS_SUBMITTED = errors.New("You have submitted the exam, you are disallowed to answer the question")
33
+ IMAGE_REQUIRED = errors.New("Image is required")
34
+ DESCRIPTION_REQUIRED = errors.New("Description is required")
35
+ CODE_REQUIRED = errors.New("Code is required")
36
 
37
  // ================= FILE UPLOAD =================
38
  FILE_TOO_LARGE = errors.New("File size exceeds the maximum limit")
provider/controller_provider.go CHANGED
@@ -12,7 +12,7 @@ type ControllerProvider interface {
12
  ProvideForgotPasswordController() controllers.ForgotPasswordController
13
  ProvideOptionController() controllers.OptionController
14
  ProvideRegionController() controllers.RegionController
15
- ProvideUploadController() *controllers.UploadController
16
  ProvideAcademyExamController() controllers.AcademyExamController
17
  }
18
 
@@ -26,7 +26,7 @@ type controllerProvider struct {
26
  forgotPasswordController controllers.ForgotPasswordController
27
  optionController controllers.OptionController
28
  regionController controllers.RegionController
29
- uploadController *controllers.UploadController
30
  academyExamController controllers.AcademyExamController
31
  }
32
 
@@ -96,7 +96,7 @@ func (c *controllerProvider) ProvideRegionController() controllers.RegionControl
96
  return c.regionController
97
  }
98
 
99
- func (c *controllerProvider) ProvideUploadController() *controllers.UploadController {
100
  return c.uploadController
101
  }
102
 
 
12
  ProvideForgotPasswordController() controllers.ForgotPasswordController
13
  ProvideOptionController() controllers.OptionController
14
  ProvideRegionController() controllers.RegionController
15
+ ProvideUploadController() controllers.UploadController
16
  ProvideAcademyExamController() controllers.AcademyExamController
17
  }
18
 
 
26
  forgotPasswordController controllers.ForgotPasswordController
27
  optionController controllers.OptionController
28
  regionController controllers.RegionController
29
+ uploadController controllers.UploadController
30
  academyExamController controllers.AcademyExamController
31
  }
32
 
 
96
  return c.regionController
97
  }
98
 
99
+ func (c *controllerProvider) ProvideUploadController() controllers.UploadController {
100
  return c.uploadController
101
  }
102
 
services/academy_service.go CHANGED
@@ -37,7 +37,6 @@ type AcademyService interface {
37
  GetAcademyResponse(ctx context.Context, accountId uuid.UUID, slug string) (*dto.AcademyDetailResponse, error)
38
  GetMaterialResponse(ctx context.Context, accountId uuid.UUID, academySlug string, materialSlug string) (*dto.MaterialDetailResponse, error)
39
  }
40
-
41
  type academyService struct {
42
  academyRepo repositories.AcademyRepository
43
  }
@@ -46,14 +45,6 @@ func NewAcademyService(academyRepo repositories.AcademyRepository) AcademyServic
46
  return &academyService{academyRepo: academyRepo}
47
  }
48
 
49
- func timePtrToString(t *time.Time) *string {
50
- if t == nil {
51
- return nil
52
- }
53
- s := t.Format(time.RFC3339)
54
- return &s
55
- }
56
-
57
  func (s *academyService) GetAcademy(ctx context.Context, accountId uuid.UUID, slug string) (entity.Academy, error) {
58
  return s.academyRepo.GetAcademyWithProgress(ctx, accountId, slug)
59
  }
@@ -64,25 +55,42 @@ func (s *academyService) GetAcademyDetail(ctx context.Context, id uuid.UUID) (en
64
  }
65
 
66
  func (s *academyService) CreateAcademy(ctx context.Context, req dto.CreateAcademyRequest) (entity.Academy, error) {
67
- if strings.TrimSpace(req.Title) == "" {
68
- return entity.Academy{}, http_error.TITLE_REQUIRED
69
- }
70
- slugVal := req.Slug
71
- if slugVal == "" {
72
- slugVal = slug.Make(req.Title)
73
- }
74
- if _, err := s.academyRepo.GetAcademyBySlug(ctx, slugVal); err == nil {
75
- return entity.Academy{}, http_error.DUPLICATE_DATA
76
- }
77
- a := entity.Academy{
78
- Id: uuid.New(),
79
- Title: req.Title,
80
- Slug: slugVal,
81
- Description: req.Description,
82
- ImageUrl: req.ImageUrl,
83
- MaterialsCount: 0,
84
- }
85
- return s.academyRepo.CreateAcademy(ctx, a)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  }
87
 
88
  func (s *academyService) UpdateAcademy(ctx context.Context, id uuid.UUID, req dto.UpdateAcademyRequest) (entity.Academy, error) {
@@ -490,6 +498,7 @@ func (s *academyService) GetAcademyResponse(ctx context.Context, accountId uuid.
490
  Id: academy.Id,
491
  Title: academy.Title,
492
  Slug: academy.Slug,
 
493
  Description: academy.Description,
494
  ImageUrl: academy.ImageUrl,
495
  MaterialsCount: academy.MaterialsCount,
@@ -500,7 +509,7 @@ func (s *academyService) GetAcademyResponse(ctx context.Context, accountId uuid.
500
  Status: academyProgress.Status,
501
  Progress: academyProgress.Progress,
502
  TotalCompletedMaterials: academyProgress.TotalCompletedMaterials,
503
- CompletedAt: timePtrToString(academyProgress.CompletedAt),
504
  },
505
  }
506
 
@@ -594,7 +603,7 @@ func (s *academyService) GetMaterialResponse(ctx context.Context, accountId uuid
594
  Progress: materialProgress.Progress,
595
  TotalCompletedContents: materialProgress.TotalCompletedContents,
596
  Status: materialProgress.Status,
597
- CompletedAt: timePtrToString(materialProgress.CompletedAt),
598
  },
599
  }
600
 
 
37
  GetAcademyResponse(ctx context.Context, accountId uuid.UUID, slug string) (*dto.AcademyDetailResponse, error)
38
  GetMaterialResponse(ctx context.Context, accountId uuid.UUID, academySlug string, materialSlug string) (*dto.MaterialDetailResponse, error)
39
  }
 
40
  type academyService struct {
41
  academyRepo repositories.AcademyRepository
42
  }
 
45
  return &academyService{academyRepo: academyRepo}
46
  }
47
 
 
 
 
 
 
 
 
 
48
  func (s *academyService) GetAcademy(ctx context.Context, accountId uuid.UUID, slug string) (entity.Academy, error) {
49
  return s.academyRepo.GetAcademyWithProgress(ctx, accountId, slug)
50
  }
 
55
  }
56
 
57
  func (s *academyService) CreateAcademy(ctx context.Context, req dto.CreateAcademyRequest) (entity.Academy, error) {
58
+ if strings.TrimSpace(req.Title) == "" {
59
+ return entity.Academy{}, http_error.TITLE_REQUIRED
60
+ }
61
+
62
+ if strings.TrimSpace(req.Code) == "" {
63
+ return entity.Academy{}, http_error.CODE_REQUIRED
64
+ }
65
+
66
+ if strings.TrimSpace(req.Description) == "" {
67
+ return entity.Academy{}, http_error.DESCRIPTION_REQUIRED
68
+ }
69
+
70
+ if strings.TrimSpace(req.ImageUrl) == "" {
71
+ return entity.Academy{}, http_error.IMAGE_REQUIRED
72
+ }
73
+
74
+ slugVal := req.Slug
75
+ if slugVal == "" {
76
+ slugVal = slug.Make(req.Title)
77
+ }
78
+
79
+ if _, err := s.academyRepo.GetAcademyBySlug(ctx, slugVal); err == nil {
80
+ return entity.Academy{}, http_error.DUPLICATE_DATA
81
+ }
82
+
83
+ a := entity.Academy{
84
+ Id: uuid.New(),
85
+ Title: req.Title,
86
+ Slug: slugVal,
87
+ Code: req.Code,
88
+ Description: req.Description,
89
+ ImageUrl: req.ImageUrl,
90
+ MaterialsCount: 0,
91
+ }
92
+
93
+ return s.academyRepo.CreateAcademy(ctx, a)
94
  }
95
 
96
  func (s *academyService) UpdateAcademy(ctx context.Context, id uuid.UUID, req dto.UpdateAcademyRequest) (entity.Academy, error) {
 
498
  Id: academy.Id,
499
  Title: academy.Title,
500
  Slug: academy.Slug,
501
+ Code: academy.Code,
502
  Description: academy.Description,
503
  ImageUrl: academy.ImageUrl,
504
  MaterialsCount: academy.MaterialsCount,
 
509
  Status: academyProgress.Status,
510
  Progress: academyProgress.Progress,
511
  TotalCompletedMaterials: academyProgress.TotalCompletedMaterials,
512
+ CompletedAt: utils.TimePtrToString(academyProgress.CompletedAt),
513
  },
514
  }
515
 
 
603
  Progress: materialProgress.Progress,
604
  TotalCompletedContents: materialProgress.TotalCompletedContents,
605
  Status: materialProgress.Status,
606
+ CompletedAt: utils.TimePtrToString(materialProgress.CompletedAt),
607
  },
608
  }
609
 
services/academy_service_helpers.go DELETED
@@ -1,353 +0,0 @@
1
- package services
2
-
3
- import (
4
- "context"
5
- "math"
6
- "time"
7
-
8
- "github.com/google/uuid"
9
-
10
- "abdanhafidz.com/go-boilerplate/models/dto"
11
- entity "abdanhafidz.com/go-boilerplate/models/entity"
12
- "abdanhafidz.com/go-boilerplate/repositories"
13
- "abdanhafidz.com/go-boilerplate/utils"
14
- )
15
-
16
- func (s *academyService) getOrCreateID(id uuid.UUID) uuid.UUID {
17
- if id == uuid.Nil {
18
- return uuid.New()
19
- }
20
- return id
21
- }
22
-
23
- func (s *academyService) calculateProgress(completed, total int64) float64 {
24
- if total <= 0 {
25
- return 0
26
- }
27
- progress := (float64(completed) / float64(total)) * 100
28
- return math.Round(progress*100) / 100
29
- }
30
-
31
- func (s *academyService) getProgressStatus(progress float64, completed, total int64) string {
32
- if progress >= 100 || (total > 0 && completed >= total) {
33
- return entity.StatusCompleted
34
- }
35
- if progress > 0 {
36
- return entity.StatusInProgress
37
- }
38
- return entity.StatusNotStarted
39
- }
40
-
41
- func (s *academyService) upsertContentProgressSimplified(
42
- ctx context.Context,
43
- txRepo repositories.AcademyRepository,
44
- accountId, academyId, materialId, contentId uuid.UUID,
45
- ) (entity.AcademyContentProgress, error) {
46
- existing, err := txRepo.GetContentProgress(ctx, accountId, academyId, materialId, contentId)
47
- if err != nil {
48
- // propagate non-not-found errors
49
- return entity.AcademyContentProgress{}, err
50
- }
51
-
52
- acp := entity.AcademyContentProgress{
53
- Id: s.getOrCreateID(existing.Id),
54
- AccountId: accountId,
55
- AcademyId: academyId,
56
- MaterialId: materialId,
57
- ContentId: contentId,
58
- Status: entity.StatusCompleted,
59
- CompletedAt: utils.Ptr(time.Now()),
60
- }
61
-
62
- if _, err := txRepo.UpsertContentProgress(ctx, acp); err != nil {
63
- return entity.AcademyContentProgress{}, err
64
- }
65
- return acp, nil
66
- }
67
-
68
- func (s *academyService) calculateMaterialProgress(
69
- ctx context.Context,
70
- txRepo repositories.AcademyRepository,
71
- accountId, academyId, materialId uuid.UUID,
72
- totalCompleted, totalContents int64,
73
- ) entity.AcademyMaterialProgress {
74
- existing, _ := txRepo.GetMaterialProgress(ctx, accountId, academyId, materialId)
75
-
76
- progress := s.calculateProgress(totalCompleted, totalContents)
77
- status := s.getProgressStatus(progress, totalCompleted, totalContents)
78
-
79
- var completedAt *time.Time
80
- if status == entity.StatusCompleted {
81
- completedAt = utils.Ptr(time.Now())
82
- progress = 100
83
- }
84
-
85
- return entity.AcademyMaterialProgress{
86
- Id: s.getOrCreateID(existing.Id),
87
- AccountId: accountId,
88
- AcademyId: academyId,
89
- MaterialId: materialId,
90
- Progress: progress,
91
- TotalCompletedContents: uint(totalCompleted),
92
- Status: status,
93
- CompletedAt: completedAt,
94
- }
95
- }
96
-
97
-
98
- func (s *academyService) calculateAcademyProgress(
99
- ctx context.Context,
100
- txRepo repositories.AcademyRepository,
101
- accountId, academyId uuid.UUID,
102
- accumulatedProgress float64,
103
- totalMaterials int64,
104
- ) entity.AcademyProgress {
105
- existing, _ := txRepo.GetAcademyProgress(ctx, accountId, academyId)
106
-
107
- var progress float64
108
- if totalMaterials > 0 {
109
- progress = math.Round((accumulatedProgress/float64(totalMaterials))*100) / 100
110
- }
111
-
112
- status := s.getProgressStatus(progress, 0, totalMaterials)
113
-
114
- var completedAt *time.Time
115
- if status == entity.StatusCompleted {
116
- completedAt = utils.Ptr(time.Now())
117
- progress = 100
118
- }
119
-
120
- totalCompleted, _ := txRepo.CountCompletedMaterialsByAcademyAndAccount(ctx, accountId, academyId)
121
-
122
- return entity.AcademyProgress{
123
- Id: s.getOrCreateID(existing.Id),
124
- AccountId: accountId,
125
- AcademyId: academyId,
126
- Progress: progress,
127
- TotalCompletedMaterials: uint(totalCompleted),
128
- Status: status,
129
- CompletedAt: completedAt,
130
- }
131
- }
132
-
133
- func (s *academyService) updateAcademyMaterialCount(ctx context.Context, txRepo repositories.AcademyRepository, academyId uuid.UUID) error {
134
- count, err := txRepo.CountMaterialsByAcademyID(ctx, academyId)
135
- if err != nil { return err }
136
- academy, err := txRepo.GetAcademyByID(ctx, academyId)
137
- if err != nil { return err }
138
- academy.MaterialsCount = count
139
- _, err = txRepo.UpdateAcademy(ctx, academy)
140
- return err
141
- }
142
-
143
-
144
- func (s *academyService) updateMaterialContentCount(ctx context.Context, txRepo repositories.AcademyRepository, materialId uuid.UUID) error {
145
- count, err := txRepo.CountContentsByMaterialID(ctx, materialId)
146
- if err != nil { return err }
147
- material, err := txRepo.GetMaterialByID(ctx, materialId)
148
- if err != nil { return err }
149
- material.ContentsCount = count
150
- _, err = txRepo.UpdateMaterial(ctx, material)
151
- return err
152
- }
153
-
154
- func formatTime(t *time.Time) *string {
155
- if t == nil {
156
- return nil
157
- }
158
- formatted := t.Format("2006-01-02T15:04:05Z07:00")
159
- return &formatted
160
- }
161
-
162
-
163
- func buildAcademyProgressResponse(ap entity.AcademyProgress) *dto.AcademyProgressResponse {
164
- if ap.Id == uuid.Nil {
165
- return nil
166
- }
167
- return &dto.AcademyProgressResponse{
168
- Id: ap.Id,
169
- AccountId: ap.AccountId,
170
- AcademyId: ap.AcademyId,
171
- Status: ap.Status,
172
- Progress: ap.Progress,
173
- TotalCompletedMaterials: ap.TotalCompletedMaterials,
174
- CompletedAt: formatTime(ap.CompletedAt),
175
- }
176
- }
177
-
178
- func buildAcademyContentResponse(content entity.AcademyContent, progress entity.AcademyContentProgress) dto.AcademyContentResponse {
179
- status := entity.StatusNotStarted
180
- if progress.Id != uuid.Nil {
181
- status = progress.Status
182
- }
183
- return dto.AcademyContentResponse{
184
- Id: content.Id,
185
- Order: content.Order,
186
- Title: content.Title,
187
- Status: status,
188
- }
189
- }
190
-
191
-
192
- func buildAcademyMaterialResponse(
193
- material entity.AcademyMaterial,
194
- contents []entity.AcademyContent,
195
- progressMap map[uuid.UUID]entity.AcademyContentProgress,
196
- progress entity.AcademyMaterialProgress,
197
- ) dto.AcademyMaterialResponse {
198
- status := entity.StatusNotStarted
199
- if progress.Id != uuid.Nil {
200
- status = progress.Status
201
- }
202
-
203
- contentDTOs := make([]dto.AcademyContentResponse, 0)
204
- for _, content := range contents {
205
- contentProgress := progressMap[content.Id]
206
- contentDTOs = append(contentDTOs, buildAcademyContentResponse(content, contentProgress))
207
- }
208
-
209
- return dto.AcademyMaterialResponse{
210
- Id: material.Id,
211
- Order: material.Order,
212
- Title: material.Title,
213
- Slug: material.Slug,
214
- Status: status,
215
- Progress: progress.Progress,
216
- TotalCompletedContents: progress.TotalCompletedContents,
217
- ContentsCount: material.ContentsCount,
218
- Contents: contentDTOs,
219
- }
220
- }
221
-
222
- func buildAcademyDetailResponse(
223
- academy entity.Academy,
224
- materials []entity.AcademyMaterial,
225
- academyProgress entity.AcademyProgress,
226
- ctx context.Context,
227
- repo repositories.AcademyRepository,
228
- ) *dto.AcademyDetailResponse {
229
- materialDTOs := make([]dto.AcademyMaterialResponse, 0)
230
-
231
- materialIds := make([]uuid.UUID, 0)
232
- contentIds := make([]uuid.UUID, 0)
233
-
234
- for _, material := range materials {
235
- materialIds = append(materialIds, material.Id)
236
- for _, content := range material.Contents {
237
- contentIds = append(contentIds, content.Id)
238
- }
239
- }
240
-
241
- materialProgressMap, _ := repo.GetMaterialProgressBatch(ctx, academyProgress.AccountId, academyProgress.AcademyId, materialIds)
242
- contentProgressMap, _ := repo.GetContentProgressBatch(ctx, academyProgress.AccountId, academyProgress.AcademyId, contentIds)
243
-
244
- for _, material := range materials {
245
- materialProgress := materialProgressMap[material.Id]
246
- contentDTOs := make([]dto.AcademyContentResponse, 0)
247
-
248
- for _, content := range material.Contents {
249
- contentProgress := contentProgressMap[content.Id]
250
- contentDTOs = append(contentDTOs, buildAcademyContentResponse(content, contentProgress))
251
- }
252
-
253
- materialDTOs = append(materialDTOs, dto.AcademyMaterialResponse{
254
- Id: material.Id,
255
- Order: material.Order,
256
- Title: material.Title,
257
- Slug: material.Slug,
258
- Status: getContentStatus(materialProgress),
259
- Progress: materialProgress.Progress,
260
- TotalCompletedContents: materialProgress.TotalCompletedContents,
261
- ContentsCount: material.ContentsCount,
262
- Contents: contentDTOs,
263
- })
264
- }
265
-
266
- return &dto.AcademyDetailResponse{
267
- Id: academy.Id,
268
- Title: academy.Title,
269
- Slug: academy.Slug,
270
- Description: academy.Description,
271
- ImageUrl: academy.ImageUrl,
272
- MaterialsCount: academy.MaterialsCount,
273
- UserProgress: buildAcademyProgressResponse(academyProgress),
274
- Materials: materialDTOs,
275
- }
276
- }
277
-
278
- func getContentStatus(progress entity.AcademyMaterialProgress) string {
279
- if progress.Id != uuid.Nil {
280
- return progress.Status
281
- }
282
- return entity.StatusNotStarted
283
- }
284
-
285
- func buildMaterialProgressResponse(mp entity.AcademyMaterialProgress) *dto.MaterialProgressResponse {
286
- if mp.Id == uuid.Nil {
287
- return nil
288
- }
289
- return &dto.MaterialProgressResponse{
290
- Id: mp.Id,
291
- AccountId: mp.AccountId,
292
- AcademyId: mp.AcademyId,
293
- MaterialId: mp.MaterialId,
294
- Progress: mp.Progress,
295
- TotalCompletedContents: mp.TotalCompletedContents,
296
- Status: mp.Status,
297
- CompletedAt: formatTime(mp.CompletedAt),
298
- }
299
- }
300
-
301
- func buildContentDetailResponse(content entity.AcademyContent, progress entity.AcademyContentProgress) dto.ContentDetailResponse {
302
- status := entity.StatusNotStarted
303
- if progress.Id != uuid.Nil {
304
- status = progress.Status
305
- }
306
-
307
- return dto.ContentDetailResponse{
308
- Id: content.Id,
309
- Order: content.Order,
310
- Title: content.Title,
311
- Status: status,
312
- }
313
- }
314
-
315
-
316
- func buildMaterialDetailResponse(
317
- material entity.AcademyMaterial,
318
- contents []entity.AcademyContent,
319
- materialProgress entity.AcademyMaterialProgress,
320
- accountId, academyId uuid.UUID,
321
- ctx context.Context,
322
- repo repositories.AcademyRepository,
323
- academySlug, materialSlug string,
324
- ) *dto.MaterialDetailResponse {
325
- contentDTOs := make([]dto.ContentDetailResponse, 0)
326
- contentIds := make([]uuid.UUID, len(contents))
327
- for i, c := range contents {
328
- contentIds[i] = c.Id
329
- }
330
-
331
- contentProgressMap, _ := repo.GetContentProgressBatch(ctx, accountId, academyId, contentIds)
332
-
333
- for _, content := range contents {
334
- contentProgress := contentProgressMap[content.Id]
335
- contentDTOs = append(contentDTOs, buildContentDetailResponse(content, contentProgress))
336
- }
337
-
338
- return &dto.MaterialDetailResponse{
339
- Id: material.Id,
340
- AcademyId: material.AcademyId,
341
- Title: material.Title,
342
- Slug: material.Slug,
343
- Description: material.Description,
344
- Order: material.Order,
345
- ContentsCount: material.ContentsCount,
346
- Progress: buildMaterialProgressResponse(materialProgress),
347
- Contents: contentDTOs,
348
- Meta: map[string]string{
349
- "academy_slug": academySlug,
350
- "material_slug": materialSlug,
351
- },
352
- }
353
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
services/upload_service.go CHANGED
@@ -6,6 +6,7 @@ import (
6
  "fmt"
7
  "io"
8
  "mime/multipart"
 
9
  "path/filepath"
10
  "regexp"
11
  "strings"
@@ -89,7 +90,8 @@ func (s *uploadService) GetFileByID(ctx context.Context, fileID uuid.UUID, accou
89
  }
90
 
91
  func (s *uploadService) processSingleFile(ctx context.Context, fileHeader *multipart.FileHeader, config config.UploadRule, uploadContext string, accountID uuid.UUID) (*entity.File, error) {
92
- if err := s.validateFile(fileHeader, config); err != nil {
 
93
  return nil, err
94
  }
95
 
@@ -103,8 +105,7 @@ func (s *uploadService) processSingleFile(ctx context.Context, fileHeader *multi
103
  }
104
  defer src.Close()
105
 
106
- contentType := fileHeader.Header.Get("Content-Type")
107
- publicURL, err := s.storageProvider.UploadFile(ctx, src, storagePath, contentType)
108
  if err != nil {
109
  return nil, http_error.UPLOAD_FAILED
110
  }
@@ -113,7 +114,7 @@ func (s *uploadService) processSingleFile(ctx context.Context, fileHeader *multi
113
  Id: uuid.New(),
114
  OriginalName: fileHeader.Filename,
115
  StoredName: storedFilename,
116
- MimeType: contentType,
117
  Size: fileHeader.Size,
118
  Path: publicURL,
119
  Context: uploadContext,
@@ -128,22 +129,70 @@ func (s *uploadService) processSingleFile(ctx context.Context, fileHeader *multi
128
  return fileEntity, nil
129
  }
130
 
131
- func (s *uploadService) validateFile(file *multipart.FileHeader, config config.UploadRule) error {
132
  if file.Size == 0 || file.Size > config.MaxBytes {
133
- return http_error.FILE_TOO_LARGE
 
 
 
 
 
 
 
 
 
 
 
 
134
  }
135
 
 
 
136
  ext := strings.ToLower(strings.TrimSpace(filepath.Ext(file.Filename)))
137
  if !config.AllowedExts[ext] {
138
- return http_error.INVALID_FILE_TYPE
 
 
 
 
139
  }
140
 
141
  blockedExts := map[string]bool{".exe": true, ".sh": true, ".bat": true, ".php": true}
142
  if blockedExts[ext] {
143
- return http_error.INVALID_FILE_TYPE
144
  }
145
 
146
- return nil
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
  }
148
 
149
  func (s *uploadService) generateStoredFilename(originalName string, ext string) string {
@@ -212,4 +261,4 @@ func (s *uploadService) UploadRawFile(ctx context.Context, reader io.Reader, ori
212
  }
213
 
214
  return fileEntity, nil
215
- }
 
6
  "fmt"
7
  "io"
8
  "mime/multipart"
9
+ "net/http"
10
  "path/filepath"
11
  "regexp"
12
  "strings"
 
90
  }
91
 
92
  func (s *uploadService) processSingleFile(ctx context.Context, fileHeader *multipart.FileHeader, config config.UploadRule, uploadContext string, accountID uuid.UUID) (*entity.File, error) {
93
+ detectedMimeType, err := s.validateFile(fileHeader, config)
94
+ if err != nil {
95
  return nil, err
96
  }
97
 
 
105
  }
106
  defer src.Close()
107
 
108
+ publicURL, err := s.storageProvider.UploadFile(ctx, src, storagePath, detectedMimeType)
 
109
  if err != nil {
110
  return nil, http_error.UPLOAD_FAILED
111
  }
 
114
  Id: uuid.New(),
115
  OriginalName: fileHeader.Filename,
116
  StoredName: storedFilename,
117
+ MimeType: detectedMimeType,
118
  Size: fileHeader.Size,
119
  Path: publicURL,
120
  Context: uploadContext,
 
129
  return fileEntity, nil
130
  }
131
 
132
+ func (s *uploadService) validateFile(file *multipart.FileHeader, config config.UploadRule) (string, error) {
133
  if file.Size == 0 || file.Size > config.MaxBytes {
134
+ return "", http_error.FILE_TOO_LARGE
135
+ }
136
+
137
+ src, err := file.Open()
138
+ if err != nil {
139
+ return "", http_error.INTERNAL_SERVER_ERROR
140
+ }
141
+ defer src.Close()
142
+
143
+ buffer := make([]byte, 512)
144
+ _, err = src.Read(buffer)
145
+ if err != nil && err != io.EOF {
146
+ return "", http_error.INTERNAL_SERVER_ERROR
147
  }
148
 
149
+ detectedMimeType := http.DetectContentType(buffer)
150
+
151
  ext := strings.ToLower(strings.TrimSpace(filepath.Ext(file.Filename)))
152
  if !config.AllowedExts[ext] {
153
+ return "", http_error.INVALID_FILE_TYPE
154
+ }
155
+
156
+ if !isValidMimeForExt(ext, detectedMimeType) {
157
+ return "", http_error.INVALID_FILE_TYPE
158
  }
159
 
160
  blockedExts := map[string]bool{".exe": true, ".sh": true, ".bat": true, ".php": true}
161
  if blockedExts[ext] {
162
+ return "", http_error.INVALID_FILE_TYPE
163
  }
164
 
165
+ return detectedMimeType, nil
166
+ }
167
+
168
+ func isValidMimeForExt(ext string, mimeType string) bool {
169
+ baseMime := strings.Split(mimeType, ";")[0]
170
+ baseMime = strings.TrimSpace(baseMime)
171
+
172
+ switch ext {
173
+ case ".jpg", ".jpeg":
174
+ return baseMime == "image/jpeg"
175
+ case ".png":
176
+ return baseMime == "image/png"
177
+ case ".gif":
178
+ return baseMime == "image/gif"
179
+ case ".pdf":
180
+ return baseMime == "application/pdf"
181
+ case ".txt":
182
+ return baseMime == "text/plain"
183
+ case ".c", ".cpp":
184
+ return baseMime == "submissions/x-c++src"
185
+ case ".py":
186
+ return baseMime == "submissions/x-python3src"
187
+ case ".java":
188
+ return baseMime == "submissions/x-java8src"
189
+ case ".cs":
190
+ return baseMime == "submissions/x-csharp8src"
191
+ case ".js":
192
+ return baseMime == "submissions/x-javascriptsrc"
193
+ default:
194
+ return false
195
+ }
196
  }
197
 
198
  func (s *uploadService) generateStoredFilename(originalName string, ext string) string {
 
261
  }
262
 
263
  return fileEntity, nil
264
+ }
utils/utils.go CHANGED
@@ -35,3 +35,11 @@ func CalculateRemainingTime(startTime, dueTime time.Time) int {
35
  func Ptr[T any](v T) *T {
36
  return &v
37
  }
 
 
 
 
 
 
 
 
 
35
  func Ptr[T any](v T) *T {
36
  return &v
37
  }
38
+
39
+ func TimePtrToString(t *time.Time) *string {
40
+ if t == nil {
41
+ return nil
42
+ }
43
+ s := t.Format(time.RFC3339)
44
+ return &s
45
+ }