lifedebugger commited on
Commit
4542d62
·
1 Parent(s): a934882

Deploy files from GitHub repository

Browse files
.gitignore CHANGED
@@ -1,2 +1,2 @@
1
- /.env
2
  go-boilerplate.exe
 
1
+ .env
2
  go-boilerplate.exe
controllers/academy_controller.go CHANGED
@@ -8,6 +8,7 @@ import (
8
  entity "abdanhafidz.com/go-boilerplate/models/entity"
9
  http_error "abdanhafidz.com/go-boilerplate/models/error"
10
  "abdanhafidz.com/go-boilerplate/services"
 
11
  "github.com/gin-gonic/gin"
12
  "github.com/google/uuid"
13
  )
@@ -86,6 +87,13 @@ func (c *academyController) ListAcademy(ctx *gin.Context) {
86
  search := ctx.DefaultQuery("search", "")
87
  sortBy := ctx.DefaultQuery("sortBy", "")
88
  order := ctx.DefaultQuery("order", "")
 
 
 
 
 
 
 
89
  isModified := false
90
 
91
  if limit < 1 {
@@ -101,8 +109,15 @@ func (c *academyController) ListAcademy(ctx *gin.Context) {
101
  isModified = true
102
  }
103
 
 
 
 
 
 
 
 
104
  offset := (page - 1) * limit
105
- p := entity.Pagination{Limit: limit, Offset: offset, Search: search, SortBy: sortBy, Order: order}
106
  list, total, err := c.academyService.ListAcademy(ctx.Request.Context(), accountId, p)
107
 
108
  if err != nil {
@@ -274,6 +289,10 @@ func (c *academyController) UpdateContentProgress(ctx *gin.Context) {
274
 
275
  func (c *academyController) JoinAcademyByCode(ctx *gin.Context) {
276
  req := RequestJSON[dto.JoinAcademyByCodeRequest](ctx)
 
 
 
 
277
  accountId := ParseAccountId(ctx)
278
  res, err := c.academyService.JoinByCode(ctx.Request.Context(), accountId, req.Code)
279
  ResponseJSON(ctx, req, res, err)
 
8
  entity "abdanhafidz.com/go-boilerplate/models/entity"
9
  http_error "abdanhafidz.com/go-boilerplate/models/error"
10
  "abdanhafidz.com/go-boilerplate/services"
11
+ "abdanhafidz.com/go-boilerplate/utils"
12
  "github.com/gin-gonic/gin"
13
  "github.com/google/uuid"
14
  )
 
87
  search := ctx.DefaultQuery("search", "")
88
  sortBy := ctx.DefaultQuery("sortBy", "")
89
  order := ctx.DefaultQuery("order", "")
90
+
91
+ var registerStatus *int
92
+ if val := ctx.Query("registerStatus"); val != "" {
93
+ if i, err := strconv.Atoi(val); err == nil {
94
+ registerStatus = &i
95
+ }
96
+ }
97
  isModified := false
98
 
99
  if limit < 1 {
 
109
  isModified = true
110
  }
111
 
112
+ var status *string
113
+ if val := ctx.Query("status"); val != "" {
114
+ if val == entity.StatusNotStarted || val == entity.StatusInProgress || val == entity.StatusFinished {
115
+ status = &val
116
+ }
117
+ }
118
+
119
  offset := (page - 1) * limit
120
+ p := entity.Pagination{Limit: limit, Offset: offset, Search: search, SortBy: sortBy, Order: order, RegisterStatus: registerStatus, Status: status}
121
  list, total, err := c.academyService.ListAcademy(ctx.Request.Context(), accountId, p)
122
 
123
  if err != nil {
 
289
 
290
  func (c *academyController) JoinAcademyByCode(ctx *gin.Context) {
291
  req := RequestJSON[dto.JoinAcademyByCodeRequest](ctx)
292
+ if err := utils.ValidateCode(req.Code); err != nil {
293
+ ResponseJSON[any, any](ctx, req, nil, http_error.INVALID_CODE)
294
+ return
295
+ }
296
  accountId := ParseAccountId(ctx)
297
  res, err := c.academyService.JoinByCode(ctx.Request.Context(), accountId, req.Code)
298
  ResponseJSON(ctx, req, res, err)
controllers/event_controller.go CHANGED
@@ -7,6 +7,7 @@ import (
7
  entity "abdanhafidz.com/go-boilerplate/models/entity"
8
  http_error "abdanhafidz.com/go-boilerplate/models/error"
9
  "abdanhafidz.com/go-boilerplate/services"
 
10
  "github.com/gin-gonic/gin"
11
  "github.com/google/uuid"
12
  )
@@ -34,6 +35,20 @@ func (c *eventController) List(ctx *gin.Context) {
34
  search := ctx.DefaultQuery("search", "")
35
  sortBy := ctx.DefaultQuery("sortBy", "")
36
  order := ctx.DefaultQuery("order", "")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  if limit < 1 {
38
  limit = 10
39
  } else if limit > 50 {
@@ -43,7 +58,7 @@ func (c *eventController) List(ctx *gin.Context) {
43
  page = 1
44
  }
45
  offset := (page - 1) * limit
46
- p := entity.Pagination{Limit: limit, Offset: offset, Search: search, SortBy: sortBy, Order: order}
47
  accountId := ParseAccountId(ctx)
48
  list, total, err := c.eventService.List(ctx.Request.Context(), accountId, p)
49
  var totalPages int
@@ -75,9 +90,13 @@ func (c *eventController) DetailBySlug(ctx *gin.Context) {
75
 
76
  func (c *eventController) Join(ctx *gin.Context) {
77
  req := RequestJSON[dto.JoinEventRequest](ctx)
 
 
 
 
78
  accountId := ParseAccountId(ctx)
79
  res, err := c.eventService.JoinByCode(ctx.Request.Context(), accountId, req.EventCode)
80
- ResponseJSON(ctx, req, res, err)
81
  }
82
 
83
  func (c *eventController) CreateEvent(ctx *gin.Context) {
 
7
  entity "abdanhafidz.com/go-boilerplate/models/entity"
8
  http_error "abdanhafidz.com/go-boilerplate/models/error"
9
  "abdanhafidz.com/go-boilerplate/services"
10
+ "abdanhafidz.com/go-boilerplate/utils"
11
  "github.com/gin-gonic/gin"
12
  "github.com/google/uuid"
13
  )
 
35
  search := ctx.DefaultQuery("search", "")
36
  sortBy := ctx.DefaultQuery("sortBy", "")
37
  order := ctx.DefaultQuery("order", "")
38
+
39
+ var registerStatus *int
40
+ if val := ctx.Query("registerStatus"); val != "" {
41
+ if i, err := strconv.Atoi(val); err == nil {
42
+ registerStatus = &i
43
+ }
44
+ }
45
+
46
+ var status *string
47
+ if val := ctx.Query("status"); val != "" {
48
+ if val == entity.EventStatusUpcoming || val == entity.EventStatusOngoing || val == entity.EventStatusEnded {
49
+ status = &val
50
+ }
51
+ }
52
  if limit < 1 {
53
  limit = 10
54
  } else if limit > 50 {
 
58
  page = 1
59
  }
60
  offset := (page - 1) * limit
61
+ p := entity.Pagination{Limit: limit, Offset: offset, Search: search, SortBy: sortBy, Order: order, RegisterStatus: registerStatus, Status: status}
62
  accountId := ParseAccountId(ctx)
63
  list, total, err := c.eventService.List(ctx.Request.Context(), accountId, p)
64
  var totalPages int
 
90
 
91
  func (c *eventController) Join(ctx *gin.Context) {
92
  req := RequestJSON[dto.JoinEventRequest](ctx)
93
+ if err := utils.ValidateCode(req.EventCode); err != nil {
94
+ ResponseJSON[any, any](ctx, req, nil, err)
95
+ return
96
+ }
97
  accountId := ParseAccountId(ctx)
98
  res, err := c.eventService.JoinByCode(ctx.Request.Context(), accountId, req.EventCode)
99
+ ResponseJSON[any, any](ctx, req, res, err)
100
  }
101
 
102
  func (c *eventController) CreateEvent(ctx *gin.Context) {
models/entity/contant.go CHANGED
@@ -1,17 +1,31 @@
1
  package models
2
 
 
 
 
 
3
  const (
4
  StatusNotStarted = "NOT_STARTED"
5
  StatusInProgress = "IN_PROGRESS"
6
- StatusFinished = "FINISHED"
 
 
 
 
 
 
7
  )
8
 
9
  const MB = 1024 * 1024
10
 
 
 
11
  type Pagination struct {
12
- Limit int
13
- Offset int
14
- Search string
15
- SortBy string
16
- Order string
17
- }
 
 
 
1
  package models
2
 
3
+ import (
4
+ "regexp"
5
+ )
6
+
7
  const (
8
  StatusNotStarted = "NOT_STARTED"
9
  StatusInProgress = "IN_PROGRESS"
10
+ StatusFinished = "FINISHED"
11
+ )
12
+
13
+ const (
14
+ EventStatusUpcoming = "UPCOMING"
15
+ EventStatusOngoing = "ONGOING"
16
+ EventStatusEnded = "ENDED"
17
  )
18
 
19
  const MB = 1024 * 1024
20
 
21
+ var CodeRegex = regexp.MustCompile(`^[a-zA-Z0-9]{6,12}$`)
22
+
23
  type Pagination struct {
24
+ Limit int
25
+ Offset int
26
+ Search string
27
+ SortBy string
28
+ Order string
29
+ RegisterStatus *int
30
+ Status *string
31
+ }
models/entity/entity.go CHANGED
@@ -86,6 +86,7 @@ type Events struct {
86
  ImgBanner string `json:"img_banner,omitempty"`
87
  EventCode string `json:"event_code,omitempty"`
88
  IsPublic bool `json:"is_public,omitempty"`
 
89
  DeletedAt gorm.DeletedAt `json:"deleted_at,omitempty" gorm:"index"`
90
  RegisterStatus int `gorm:"-" json:"register_status"`
91
  }
@@ -272,6 +273,7 @@ type Academy struct {
272
  Materials []AcademyMaterial `gorm:"foreignKey:AcademyId;references:Id" json:"materials,omitempty"`
273
  AcademyProgress AcademyProgress `gorm:"foreignKey:AcademyId;references:Id" json:"academy_progress,omitempty"`
274
  RegisterStatus int `gorm:"-" json:"register_status"`
 
275
  DeletedAt gorm.DeletedAt `json:"deleted_at,omitempty" gorm:"index"`
276
  }
277
 
@@ -287,6 +289,7 @@ type AcademyMaterial struct {
287
  ContentsCount int64 `json:"contents_count,omitempty"`
288
  Contents []AcademyContent `gorm:"foreignKey:MaterialId;references:Id" json:"contents,omitempty"`
289
  AcademyMaterialProgress AcademyMaterialProgress `gorm:"foreignKey:MaterialId;references:Id" json:"academy_material_progress,omitempty"`
 
290
  DeletedAt gorm.DeletedAt `json:"deleted_at,omitempty" gorm:"index"`
291
  }
292
 
@@ -299,6 +302,7 @@ type AcademyContent struct {
299
  Order uint `json:"order,omitempty"`
300
  Contents string `json:"contents,omitempty"`
301
  AcademyContentProgress AcademyContentProgress `gorm:"foreignKey:ContentId;references:Id" json:"academy_content_progress,omitempty"`
 
302
  DeletedAt gorm.DeletedAt `json:"deleted_at,omitempty" gorm:"index"`
303
  }
304
 
 
86
  ImgBanner string `json:"img_banner,omitempty"`
87
  EventCode string `json:"event_code,omitempty"`
88
  IsPublic bool `json:"is_public,omitempty"`
89
+ CreatedAt time.Time `json:"created_at,omitempty"`
90
  DeletedAt gorm.DeletedAt `json:"deleted_at,omitempty" gorm:"index"`
91
  RegisterStatus int `gorm:"-" json:"register_status"`
92
  }
 
273
  Materials []AcademyMaterial `gorm:"foreignKey:AcademyId;references:Id" json:"materials,omitempty"`
274
  AcademyProgress AcademyProgress `gorm:"foreignKey:AcademyId;references:Id" json:"academy_progress,omitempty"`
275
  RegisterStatus int `gorm:"-" json:"register_status"`
276
+ CreatedAt time.Time `json:"created_at,omitempty"`
277
  DeletedAt gorm.DeletedAt `json:"deleted_at,omitempty" gorm:"index"`
278
  }
279
 
 
289
  ContentsCount int64 `json:"contents_count,omitempty"`
290
  Contents []AcademyContent `gorm:"foreignKey:MaterialId;references:Id" json:"contents,omitempty"`
291
  AcademyMaterialProgress AcademyMaterialProgress `gorm:"foreignKey:MaterialId;references:Id" json:"academy_material_progress,omitempty"`
292
+ CreatedAt time.Time `json:"created_at,omitempty"`
293
  DeletedAt gorm.DeletedAt `json:"deleted_at,omitempty" gorm:"index"`
294
  }
295
 
 
302
  Order uint `json:"order,omitempty"`
303
  Contents string `json:"contents,omitempty"`
304
  AcademyContentProgress AcademyContentProgress `gorm:"foreignKey:ContentId;references:Id" json:"academy_content_progress,omitempty"`
305
+ CreatedAt time.Time `json:"created_at,omitempty"`
306
  DeletedAt gorm.DeletedAt `json:"deleted_at,omitempty" gorm:"index"`
307
  }
308
 
models/error/error.go CHANGED
@@ -36,7 +36,7 @@ var (
36
  IMAGE_REQUIRED = errors.New("Image is required")
37
  DESCRIPTION_REQUIRED = errors.New("Description is required")
38
  CODE_REQUIRED = errors.New("Code is required")
39
- INVALID_CODE = errors.New("Code is invalid, it must be 6 characters long and contain only letters or numbers")
40
  EVENT_START_DATE_INVALID = errors.New("Event start date must be in the future")
41
  EVENT_END_DATE_INVALID = errors.New("Event end date must be after start date")
42
  INVALID_DATE_FORMAT = errors.New("Invalid date format, please use RFC3339")
 
36
  IMAGE_REQUIRED = errors.New("Image is required")
37
  DESCRIPTION_REQUIRED = errors.New("Description is required")
38
  CODE_REQUIRED = errors.New("Code is required")
39
+ INVALID_CODE = errors.New("Code must be on range 6-12")
40
  EVENT_START_DATE_INVALID = errors.New("Event start date must be in the future")
41
  EVENT_END_DATE_INVALID = errors.New("Event end date must be after start date")
42
  INVALID_DATE_FORMAT = errors.New("Invalid date format, please use RFC3339")
repositories/academy_repository.go CHANGED
@@ -159,36 +159,44 @@ func (r *academyRepository) ListVisibleAcademy(ctx context.Context, accountId uu
159
 
160
  sub := r.db.WithContext(ctx).Model(&entity.AcademyAssign{}).Select("academy_id").Where("account_id = ?", accountId)
161
 
 
162
  q := r.db.WithContext(ctx).Model(&entity.Academy{}).
163
- Where(r.db.Where("is_public = ?", true).Or("id IN (?)", sub))
 
 
 
 
 
 
 
 
 
 
 
164
 
165
  if p != nil {
 
 
 
 
 
 
 
 
 
166
  sortColumn := "title"
167
  switch p.SortBy {
168
- case "title":
169
- sortColumn = "title"
170
- case "slug":
171
- sortColumn = "slug"
172
- case "description":
173
- sortColumn = "description"
174
- case "createdAt", "created_at":
175
- sortColumn = "created_at"
176
- case "updatedAt", "updated_at":
177
- sortColumn = "updated_at"
178
- case "materials_count", "materialsCount":
179
- sortColumn = "materials_count"
180
  }
181
 
182
  if s := strings.TrimSpace(p.Search); s != "" {
183
  s = strings.Trim(s, "\"'")
184
  s = strings.ToLower(s)
185
  like := "%" + s + "%"
186
-
187
- if p.SortBy != "" {
188
- q = q.Where(fmt.Sprintf("LOWER(%s) LIKE ?", sortColumn), like)
189
- } else {
190
- q = q.Where("LOWER(title) LIKE ? OR LOWER(slug) LIKE ?", like, like)
191
- }
192
  }
193
 
194
  sortDirection := "DESC"
@@ -196,8 +204,13 @@ func (r *academyRepository) ListVisibleAcademy(ctx context.Context, accountId uu
196
  sortDirection = "ASC"
197
  }
198
 
 
199
  q = q.Order(fmt.Sprintf("%s %s", sortColumn, sortDirection))
200
 
 
 
 
 
201
  if p.Limit > 0 {
202
  q = q.Limit(p.Limit)
203
  }
@@ -206,10 +219,6 @@ func (r *academyRepository) ListVisibleAcademy(ctx context.Context, accountId uu
206
  }
207
  }
208
 
209
- if err := q.Count(&total).Error; err != nil {
210
- return nil, 0, err
211
- }
212
-
213
  if err := q.Find(&list).Error; err != nil {
214
  return nil, 0, err
215
  }
 
159
 
160
  sub := r.db.WithContext(ctx).Model(&entity.AcademyAssign{}).Select("academy_id").Where("account_id = ?", accountId)
161
 
162
+ // Base query
163
  q := r.db.WithContext(ctx).Model(&entity.Academy{}).
164
+ Where(r.db.Where("academy.is_public = ?", true).Or("academy.id IN (?)", sub))
165
+
166
+ // Join for status filtering
167
+ if p != nil && p.Status != nil {
168
+ q = q.Joins("LEFT JOIN academy_progress ap ON ap.academy_id = academy.id AND ap.account_id = ?", accountId)
169
+ status := *p.Status
170
+ if status == entity.StatusNotStarted {
171
+ q = q.Where("ap.status IS NULL OR ap.status = ?", entity.StatusNotStarted)
172
+ } else {
173
+ q = q.Where("ap.status = ?", status)
174
+ }
175
+ }
176
 
177
  if p != nil {
178
+ if p.RegisterStatus != nil {
179
+ switch *p.RegisterStatus {
180
+ case 1:
181
+ q = q.Where("academy.id IN (?)", sub)
182
+ case 0:
183
+ q = q.Where("academy.id NOT IN (?)", sub)
184
+ }
185
+ }
186
+
187
  sortColumn := "title"
188
  switch p.SortBy {
189
+ case "title":
190
+ sortColumn = "title"
191
+ case "createdAt", "created_at":
192
+ sortColumn = "created_at"
 
 
 
 
 
 
 
 
193
  }
194
 
195
  if s := strings.TrimSpace(p.Search); s != "" {
196
  s = strings.Trim(s, "\"'")
197
  s = strings.ToLower(s)
198
  like := "%" + s + "%"
199
+ q = q.Where("LOWER(academy.title) LIKE ? OR LOWER(academy.slug) LIKE ?", like, like)
 
 
 
 
 
200
  }
201
 
202
  sortDirection := "DESC"
 
204
  sortDirection = "ASC"
205
  }
206
 
207
+ // Disambiguate sort column
208
  q = q.Order(fmt.Sprintf("%s %s", sortColumn, sortDirection))
209
 
210
+ if err := q.Count(&total).Error; err != nil {
211
+ return nil, 0, err
212
+ }
213
+
214
  if p.Limit > 0 {
215
  q = q.Limit(p.Limit)
216
  }
 
219
  }
220
  }
221
 
 
 
 
 
222
  if err := q.Find(&list).Error; err != nil {
223
  return nil, 0, err
224
  }
repositories/event_repository.go CHANGED
@@ -3,6 +3,7 @@ package repositories
3
  import (
4
  "context"
5
  "strings"
 
6
 
7
  entity "abdanhafidz.com/go-boilerplate/models/entity"
8
  "github.com/google/uuid"
@@ -87,10 +88,8 @@ func (r *eventsRepository) GetAllPaginate(ctx context.Context, p entity.Paginati
87
  ord = "asc"
88
  }
89
  switch col {
90
- case "title", "slug", "start_event", "end_event", "event_code", "overview", "is_public":
91
  q = q.Order(col + " " + ord)
92
- case "id_event", "id":
93
- q = q.Order("id " + ord)
94
  default:
95
  q = q.Order("title " + ord)
96
  }
@@ -129,8 +128,10 @@ func (r *eventsRepository) ListPublic(ctx context.Context, p *entity.Pagination)
129
  ord = "asc"
130
  }
131
  switch col {
132
- case "title", "slug", "start_event", "end_event", "event_code", "overview", "is_public":
133
  q = q.Order(col + " " + ord)
 
 
134
  case "id_event", "id":
135
  q = q.Order("id " + ord)
136
  default:
@@ -163,6 +164,27 @@ func (r *eventsRepository) ListVisible(ctx context.Context, accountId uuid.UUID,
163
  }
164
 
165
  if p != nil {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
  if s := strings.TrimSpace(p.Search); s != "" {
167
  s = strings.Trim(s, "\"'")
168
  s = strings.ToLower(s)
@@ -182,23 +204,25 @@ func (r *eventsRepository) ListVisible(ctx context.Context, accountId uuid.UUID,
182
  ord = "asc"
183
  }
184
  switch col {
185
- case "title", "slug", "start_event", "end_event", "overview":
186
  q = q.Order(col + " " + ord)
 
 
187
  case "id_event", "id":
188
  q = q.Order("id " + ord)
189
  default:
190
  q = q.Order("title " + ord)
191
  }
 
 
 
 
192
  if p.Limit > 0 {
193
  q = q.Limit(p.Limit)
194
  }
195
  if p.Offset > 0 {
196
  q = q.Offset(p.Offset)
197
  }
198
- } else {
199
- if err := q.Count(&total).Error; err != nil {
200
- return nil, 0, err
201
- }
202
  }
203
 
204
  if err := q.Find(&list).Error; err != nil {
 
3
  import (
4
  "context"
5
  "strings"
6
+ "time"
7
 
8
  entity "abdanhafidz.com/go-boilerplate/models/entity"
9
  "github.com/google/uuid"
 
88
  ord = "asc"
89
  }
90
  switch col {
91
+ case "title", "start_event", "end_event", "overview", "is_public", "created_at":
92
  q = q.Order(col + " " + ord)
 
 
93
  default:
94
  q = q.Order("title " + ord)
95
  }
 
128
  ord = "asc"
129
  }
130
  switch col {
131
+ case "title", "slug", "start_event", "end_event", "event_code", "overview", "is_public", "created_at":
132
  q = q.Order(col + " " + ord)
133
+ case "createdat":
134
+ q = q.Order("created_at " + ord)
135
  case "id_event", "id":
136
  q = q.Order("id " + ord)
137
  default:
 
164
  }
165
 
166
  if p != nil {
167
+ if p.RegisterStatus != nil {
168
+ switch *p.RegisterStatus {
169
+ case 1:
170
+ q = q.Where("academy.id IN (?)", sub)
171
+ case 0:
172
+ q = q.Where("academy.id NOT IN (?)", sub)
173
+ }
174
+ }
175
+
176
+ if p.Status != nil {
177
+ now := time.Now()
178
+ switch *p.Status {
179
+ case entity.EventStatusUpcoming:
180
+ q = q.Where("start_event > ?", now)
181
+ case entity.EventStatusOngoing:
182
+ q = q.Where("start_event <= ? AND end_event >= ?", now, now)
183
+ case entity.EventStatusEnded:
184
+ q = q.Where("end_event < ?", now)
185
+ }
186
+ }
187
+
188
  if s := strings.TrimSpace(p.Search); s != "" {
189
  s = strings.Trim(s, "\"'")
190
  s = strings.ToLower(s)
 
204
  ord = "asc"
205
  }
206
  switch col {
207
+ case "title", "slug", "start_event", "end_event", "overview", "created_at":
208
  q = q.Order(col + " " + ord)
209
+ case "createdat":
210
+ q = q.Order("created_at " + ord)
211
  case "id_event", "id":
212
  q = q.Order("id " + ord)
213
  default:
214
  q = q.Order("title " + ord)
215
  }
216
+ if err := q.Count(&total).Error; err != nil {
217
+ return nil, 0, err
218
+ }
219
+
220
  if p.Limit > 0 {
221
  q = q.Limit(p.Limit)
222
  }
223
  if p.Offset > 0 {
224
  q = q.Offset(p.Offset)
225
  }
 
 
 
 
226
  }
227
 
228
  if err := q.Find(&list).Error; err != nil {
router/academy_router.go CHANGED
@@ -9,34 +9,16 @@ import (
9
  func AcademyRouter(router *gin.Engine, middleware provider.MiddlewareProvider, controller provider.ControllerProvider) {
10
  academyController := controller.ProvideAcademyController()
11
  authenticationMiddleware := middleware.ProvideAuthenticationMiddleware()
12
- authorizationMiddleware := middleware.ProvideAuthorizationMiddleware()
13
  routerGroup := router.Group("/api/v1/academy")
14
 
15
  routerGroup.Use(gzip.Gzip(gzip.DefaultCompression))
16
  {
17
- adminGroup := routerGroup.Group("/admin", authorizationMiddleware.VerifyAdmin)
18
-
19
- adminGroup.POST("/", academyController.CreateAcademy)
20
- adminGroup.GET("/id/:id/detail", academyController.GetAcademyDetail)
21
- adminGroup.PUT("/id/:id", academyController.UpdateAcademy)
22
- adminGroup.DELETE("/id/:id", academyController.DeleteAcademy)
23
-
24
- adminGroup.POST("/materials", academyController.CreateMaterial)
25
- adminGroup.DELETE("/materials/:id", academyController.DeleteMaterial)
26
-
27
- adminGroup.POST("/contents", academyController.CreateContent)
28
- adminGroup.DELETE("/contents/:id", academyController.DeleteContent)
29
-
30
- adminGroup.POST("/assign", academyController.AssignAccountToAcademy)
31
- adminGroup.DELETE("/assign/:id", academyController.UnassignAccountFromAcademy)
32
- adminGroup.GET("/assign/:academy_id", academyController.ListAssignmentsByAcademy)
33
  }
34
-
35
- routerGroup.GET("/", authenticationMiddleware.VerifyAccount, academyController.ListAcademy)
36
- routerGroup.POST("/join", authenticationMiddleware.VerifyAccount, academyController.JoinAcademyByCode)
37
- routerGroup.GET("/:academy_slug", authenticationMiddleware.VerifyAccount, academyController.GetAcademy)
38
- routerGroup.GET("/:academy_slug/:material_slug", authenticationMiddleware.VerifyAccount, academyController.GetMaterial)
39
- routerGroup.GET("/:academy_slug/:material_slug/:order", authenticationMiddleware.VerifyAccount, academyController.GetContent)
40
-
41
- routerGroup.POST("/:academy_slug/:material_slug/:order", authenticationMiddleware.VerifyAccount, academyController.UpdateContentProgress)
42
  }
 
9
  func AcademyRouter(router *gin.Engine, middleware provider.MiddlewareProvider, controller provider.ControllerProvider) {
10
  academyController := controller.ProvideAcademyController()
11
  authenticationMiddleware := middleware.ProvideAuthenticationMiddleware()
12
+
13
  routerGroup := router.Group("/api/v1/academy")
14
 
15
  routerGroup.Use(gzip.Gzip(gzip.DefaultCompression))
16
  {
17
+ routerGroup.GET("/", authenticationMiddleware.VerifyAccount, academyController.ListAcademy)
18
+ routerGroup.POST("/join", authenticationMiddleware.VerifyAccount, academyController.JoinAcademyByCode)
19
+ routerGroup.GET("/:academy_slug", authenticationMiddleware.VerifyAccount, academyController.GetAcademy)
20
+ routerGroup.GET("/:academy_slug/:material_slug", authenticationMiddleware.VerifyAccount, academyController.GetMaterial)
21
+ routerGroup.GET("/:academy_slug/:material_slug/:order", authenticationMiddleware.VerifyAccount, academyController.GetContent)
22
+ routerGroup.POST("/:academy_slug/:material_slug/:order", authenticationMiddleware.VerifyAccount, academyController.UpdateContentProgress)
 
 
 
 
 
 
 
 
 
 
23
  }
 
 
 
 
 
 
 
 
24
  }
router/admin_router.go ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package router
2
+
3
+ import (
4
+ "abdanhafidz.com/go-boilerplate/provider"
5
+ "github.com/gin-gonic/gin"
6
+ )
7
+
8
+ func AdminRouter(router *gin.Engine, middleware provider.MiddlewareProvider, controller provider.ControllerProvider) {
9
+ authenticationMiddleware := middleware.ProvideAuthenticationMiddleware()
10
+ authorizationMiddleware := middleware.ProvideAuthorizationMiddleware()
11
+
12
+ eventController := controller.ProvideEventController()
13
+ academyController := controller.ProvideAcademyController()
14
+ authenticationController := controller.ProvideAuthenticationController()
15
+
16
+ // Event Admin Routes
17
+ eventAdminGroup := router.Group("api/v1/admin/events", authenticationMiddleware.VerifyAccount, authorizationMiddleware.VerifyAdmin)
18
+ {
19
+ eventAdminGroup.POST("/", eventController.CreateEvent)
20
+ eventAdminGroup.PUT("/:id", eventController.UpdateEvent)
21
+ eventAdminGroup.DELETE("/:id", eventController.DeleteEvent)
22
+ }
23
+
24
+ // Academy Admin Routes
25
+ academyAdminGroup := router.Group("/api/v1/admin/academy", authenticationMiddleware.VerifyAccount, authorizationMiddleware.VerifyAdmin)
26
+ {
27
+ academyAdminGroup.POST("/", academyController.CreateAcademy)
28
+ academyAdminGroup.GET("/id/:id/detail", academyController.GetAcademyDetail)
29
+ academyAdminGroup.PUT("/id/:id", academyController.UpdateAcademy)
30
+ academyAdminGroup.DELETE("/id/:id", academyController.DeleteAcademy)
31
+
32
+ academyAdminGroup.POST("/materials", academyController.CreateMaterial)
33
+ academyAdminGroup.DELETE("/materials/:id", academyController.DeleteMaterial)
34
+
35
+ academyAdminGroup.POST("/contents", academyController.CreateContent)
36
+ academyAdminGroup.DELETE("/contents/:id", academyController.DeleteContent)
37
+
38
+ academyAdminGroup.POST("/assign", academyController.AssignAccountToAcademy)
39
+ academyAdminGroup.DELETE("/assign/:id", academyController.UnassignAccountFromAcademy)
40
+ academyAdminGroup.GET("/assign/:academy_id", academyController.ListAssignmentsByAcademy)
41
+ }
42
+
43
+ // Authentication Admin Routes
44
+ authAdminGroup := router.Group("/api/v1/admin/authentication", authenticationMiddleware.VerifyAccount, authorizationMiddleware.VerifySuperAdmin)
45
+ {
46
+ authAdminGroup.PUT("/:account_id/assign", authenticationController.UpdateUserRole)
47
+ }
48
+ }
router/authentication_router.go CHANGED
@@ -2,6 +2,7 @@ package router
2
 
3
  import (
4
  "abdanhafidz.com/go-boilerplate/provider"
 
5
  "github.com/gin-gonic/gin"
6
  )
7
 
@@ -9,16 +10,12 @@ func AuthenticationRouter(router *gin.Engine, middleware provider.MiddlewareProv
9
  routerGroup := router.Group("/api/v1/authentication")
10
  authenticationController := controller.ProvideAuthenticationController()
11
  authenticationmiddleware := middleware.ProvideAuthenticationMiddleware()
12
- authorizationMiddleware := middleware.ProvideAuthorizationMiddleware()
 
13
  {
14
  routerGroup.POST("/external-login", authenticationController.ExternalAuth)
15
  routerGroup.POST("/login", authenticationController.SignIn)
16
  routerGroup.POST("/register", authenticationController.SignUp)
17
  routerGroup.PUT("/change-password", authenticationmiddleware.VerifyAccount, authenticationController.ChangePassword)
18
  }
19
-
20
- userGroup := router.Group("/api/v1/users")
21
- {
22
- userGroup.PUT("/:account_id/assign", authorizationMiddleware.VerifySuperAdmin, authenticationController.UpdateUserRole)
23
- }
24
  }
 
2
 
3
  import (
4
  "abdanhafidz.com/go-boilerplate/provider"
5
+ "github.com/gin-contrib/gzip"
6
  "github.com/gin-gonic/gin"
7
  )
8
 
 
10
  routerGroup := router.Group("/api/v1/authentication")
11
  authenticationController := controller.ProvideAuthenticationController()
12
  authenticationmiddleware := middleware.ProvideAuthenticationMiddleware()
13
+
14
+ routerGroup.Use(gzip.Gzip(gzip.DefaultCompression))
15
  {
16
  routerGroup.POST("/external-login", authenticationController.ExternalAuth)
17
  routerGroup.POST("/login", authenticationController.SignIn)
18
  routerGroup.POST("/register", authenticationController.SignUp)
19
  routerGroup.PUT("/change-password", authenticationmiddleware.VerifyAccount, authenticationController.ChangePassword)
20
  }
 
 
 
 
 
21
  }
router/event_router.go CHANGED
@@ -2,22 +2,20 @@ package router
2
 
3
  import (
4
  "abdanhafidz.com/go-boilerplate/provider"
 
5
  "github.com/gin-gonic/gin"
6
  )
7
 
8
  func EventRouter(router *gin.Engine, middleware provider.MiddlewareProvider, controller provider.ControllerProvider) {
9
  eventController := controller.ProvideEventController()
10
  authenticationMiddleware := middleware.ProvideAuthenticationMiddleware()
11
- authorizationMiddleware := middleware.ProvideAuthorizationMiddleware()
12
  routerGroup := router.Group("api/v1/events")
 
 
13
  {
14
  routerGroup.GET("/", authenticationMiddleware.VerifyAccount, eventController.List)
15
  routerGroup.GET("/:event_slug", authenticationMiddleware.VerifyAccount, eventController.DetailBySlug)
16
  routerGroup.POST("/register-event", authenticationMiddleware.VerifyAccount, eventController.Join)
17
-
18
- adminGroup := routerGroup.Group("/admin", authorizationMiddleware.VerifyAdmin)
19
- adminGroup.POST("/", eventController.CreateEvent)
20
- adminGroup.PUT("/:id", eventController.UpdateEvent)
21
- adminGroup.DELETE("/:id", eventController.DeleteEvent)
22
  }
23
  }
 
2
 
3
  import (
4
  "abdanhafidz.com/go-boilerplate/provider"
5
+ "github.com/gin-contrib/gzip"
6
  "github.com/gin-gonic/gin"
7
  )
8
 
9
  func EventRouter(router *gin.Engine, middleware provider.MiddlewareProvider, controller provider.ControllerProvider) {
10
  eventController := controller.ProvideEventController()
11
  authenticationMiddleware := middleware.ProvideAuthenticationMiddleware()
12
+
13
  routerGroup := router.Group("api/v1/events")
14
+
15
+ routerGroup.Use(gzip.Gzip(gzip.DefaultCompression))
16
  {
17
  routerGroup.GET("/", authenticationMiddleware.VerifyAccount, eventController.List)
18
  routerGroup.GET("/:event_slug", authenticationMiddleware.VerifyAccount, eventController.DetailBySlug)
19
  routerGroup.POST("/register-event", authenticationMiddleware.VerifyAccount, eventController.Join)
 
 
 
 
 
20
  }
21
  }
router/router.go CHANGED
@@ -13,8 +13,9 @@ func RunRouter(appProvider provider.AppProvider) {
13
  EventRouter(router, middleware, controller)
14
  OptionsRouter(router, controller)
15
  AcademyRouter(router, middleware, controller)
16
- ExamEventRouter(router, middleware, controller)
17
- AcademyExamRouter(router, middleware, controller)
18
- UploadRouter(router,middleware, controller)
19
- router.Run(config.ProvideEnvConfig().GetTCPAddress())
 
20
  }
 
13
  EventRouter(router, middleware, controller)
14
  OptionsRouter(router, controller)
15
  AcademyRouter(router, middleware, controller)
16
+ ExamEventRouter(router, middleware, controller)
17
+ AcademyExamRouter(router, middleware, controller)
18
+ UploadRouter(router, middleware, controller)
19
+ AdminRouter(router, middleware, controller)
20
+ router.Run(config.ProvideEnvConfig().GetTCPAddress())
21
  }
services/academy_service.go CHANGED
@@ -79,13 +79,8 @@ func (s *academyService) CreateAcademy(ctx context.Context, req dto.CreateAcadem
79
  if strings.TrimSpace(req.ImageUrl) == "" {
80
  return entity.Academy{}, http_error.IMAGE_REQUIRED
81
  }
82
- if len(req.Code) != 6 {
83
- return entity.Academy{}, http_error.INVALID_CODE
84
- }
85
- for i := 0; i < 6; i++ {
86
- if !((req.Code[i] >= 'A' && req.Code[i] <= 'Z') || (req.Code[i] >= '0' && req.Code[i] <= '9')) {
87
- return entity.Academy{}, http_error.INVALID_CODE
88
- }
89
  }
90
 
91
  slugVal := req.Slug
@@ -824,15 +819,6 @@ func (s *academyService) ListAssignmentsByAcademy(ctx context.Context, academyId
824
  return s.academyRepo.ListAssignmentsByAcademy(ctx, academyId)
825
  }
826
  func (s *academyService) JoinByCode(ctx context.Context, accountId uuid.UUID, code string) (entity.AcademyAssign, error) {
827
- if len(code) != 6 {
828
- return entity.AcademyAssign{}, http_error.BAD_REQUEST_ERROR
829
- }
830
- for i := 0; i < 6; i++ {
831
- ch := code[i]
832
- if !((ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9')) {
833
- return entity.AcademyAssign{}, http_error.BAD_REQUEST_ERROR
834
- }
835
- }
836
  ac, err := s.academyRepo.GetAcademyByCode(ctx, code)
837
  if err != nil {
838
  return entity.AcademyAssign{}, http_error.ACADEMY_NOT_FOUND
 
79
  if strings.TrimSpace(req.ImageUrl) == "" {
80
  return entity.Academy{}, http_error.IMAGE_REQUIRED
81
  }
82
+ if err := utils.ValidateCode(req.Code); err != nil {
83
+ return entity.Academy{}, err
 
 
 
 
 
84
  }
85
 
86
  slugVal := req.Slug
 
819
  return s.academyRepo.ListAssignmentsByAcademy(ctx, academyId)
820
  }
821
  func (s *academyService) JoinByCode(ctx context.Context, accountId uuid.UUID, code string) (entity.AcademyAssign, error) {
 
 
 
 
 
 
 
 
 
822
  ac, err := s.academyRepo.GetAcademyByCode(ctx, code)
823
  if err != nil {
824
  return entity.AcademyAssign{}, http_error.ACADEMY_NOT_FOUND
services/event_service.go CHANGED
@@ -9,6 +9,7 @@ import (
9
  entity "abdanhafidz.com/go-boilerplate/models/entity"
10
  http_error "abdanhafidz.com/go-boilerplate/models/error"
11
  "abdanhafidz.com/go-boilerplate/repositories"
 
12
  "github.com/google/uuid"
13
  )
14
 
@@ -104,7 +105,7 @@ func (s *eventService) GetStatus(ctx context.Context, slug string, accountId uui
104
  event, err := s.DetailBySlug(ctx, slug, accountId)
105
  currentTime := time.Now()
106
  eventStatus.IsHasNotStarted = currentTime.Before(event.Data.StartEvent)
107
- eventStatus.IsFinished = currentTime.Before(event.Data.EndEvent)
108
  eventStatus.IsOnGoing = !(eventStatus.IsHasNotStarted) && !(eventStatus.IsFinished)
109
  return eventStatus, err
110
  }
@@ -125,14 +126,8 @@ func (s *eventService) CreateEvent(ctx context.Context, req dto.CreateEventReque
125
  return entity.Events{}, http_error.INVALID_DATE_FORMAT
126
  }
127
 
128
- if len(req.EventCode) != 6 {
129
- return entity.Events{}, http_error.INVALID_CODE
130
- }
131
- for i := 0; i < 6; i++ {
132
- c := req.EventCode[i]
133
- if !((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')) {
134
- return entity.Events{}, http_error.INVALID_CODE
135
- }
136
  }
137
 
138
  slugVal := strings.ToLower(strings.ReplaceAll(req.Title, " ", "-"))
 
9
  entity "abdanhafidz.com/go-boilerplate/models/entity"
10
  http_error "abdanhafidz.com/go-boilerplate/models/error"
11
  "abdanhafidz.com/go-boilerplate/repositories"
12
+ "abdanhafidz.com/go-boilerplate/utils"
13
  "github.com/google/uuid"
14
  )
15
 
 
105
  event, err := s.DetailBySlug(ctx, slug, accountId)
106
  currentTime := time.Now()
107
  eventStatus.IsHasNotStarted = currentTime.Before(event.Data.StartEvent)
108
+ eventStatus.IsFinished = currentTime.After(event.Data.EndEvent)
109
  eventStatus.IsOnGoing = !(eventStatus.IsHasNotStarted) && !(eventStatus.IsFinished)
110
  return eventStatus, err
111
  }
 
126
  return entity.Events{}, http_error.INVALID_DATE_FORMAT
127
  }
128
 
129
+ if err := utils.ValidateCode(req.EventCode); err != nil {
130
+ return entity.Events{}, err
 
 
 
 
 
 
131
  }
132
 
133
  slugVal := strings.ToLower(strings.ReplaceAll(req.Title, " ", "-"))
services/exam_event_service.go CHANGED
@@ -125,13 +125,14 @@ func (s *examService) GetExamEventAttempt(ctx context.Context, eventSlug string,
125
 
126
  examEventAttempt, err := s.examEventAttemptRepo.GetByExamEvent(ctx, ev.Data.Id, exam.Id, accountId)
127
  fmt.Println("Error Exam Event Attempt", errors.Is(err, gorm.ErrRecordNotFound))
 
128
  if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
129
  return dto.UserExamStatus{}, entity.ExamEventAttempt{}, err
130
  }
131
 
132
  var attemptStatus dto.UserExamStatus
133
  attemptStatus.IsNotAttempt = errors.Is(err, gorm.ErrRecordNotFound)
134
- attemptStatus.IsTimeOut = (utils.CalculateRemainingTime(examEventAttempt.CreatedAt, examEventAttempt.DueAt) == 0) || false
135
  attemptStatus.IsSubmitted = examEventAttempt.Submitted
136
  attemptStatus.IsOnAttempt = !attemptStatus.IsNotAttempt && !attemptStatus.IsTimeOut && !attemptStatus.IsSubmitted
137
  return attemptStatus, examEventAttempt, nil
@@ -235,7 +236,9 @@ func (s *examService) AttemptExamEvent(ctx context.Context, eventSlug string, ex
235
  return entity.ExamEventAttempt{}, err
236
  }
237
  attemptStatus, examEventAttempt, err := s.GetExamEventAttempt(ctx, eventSlug, examSlug, accountId)
238
-
 
 
239
  if err != nil {
240
  return entity.ExamEventAttempt{}, err
241
  }
@@ -246,10 +249,11 @@ func (s *examService) AttemptExamEvent(ctx context.Context, eventSlug string, ex
246
  if err != nil {
247
  return entity.ExamEventAttempt{}, err
248
  }
 
249
  if attemptStatus.IsNotAttempt {
250
 
251
  if eventStatus.IsFinished {
252
- return entity.ExamEventAttempt{}, err.EVENT_FINISHED
253
  }
254
 
255
  startTime, dueTime := s.SetupExamTimer(ctx, exam)
@@ -406,3 +410,4 @@ func (s *examService) AnswerExamEvent(ctx context.Context, eventSlug string, att
406
  return CPQuestionVerdict, err
407
  }
408
 
 
 
125
 
126
  examEventAttempt, err := s.examEventAttemptRepo.GetByExamEvent(ctx, ev.Data.Id, exam.Id, accountId)
127
  fmt.Println("Error Exam Event Attempt", errors.Is(err, gorm.ErrRecordNotFound))
128
+
129
  if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
130
  return dto.UserExamStatus{}, entity.ExamEventAttempt{}, err
131
  }
132
 
133
  var attemptStatus dto.UserExamStatus
134
  attemptStatus.IsNotAttempt = errors.Is(err, gorm.ErrRecordNotFound)
135
+ attemptStatus.IsTimeOut = !attemptStatus.IsNotAttempt && (utils.CalculateRemainingTime(examEventAttempt.CreatedAt, examEventAttempt.DueAt) == 0)
136
  attemptStatus.IsSubmitted = examEventAttempt.Submitted
137
  attemptStatus.IsOnAttempt = !attemptStatus.IsNotAttempt && !attemptStatus.IsTimeOut && !attemptStatus.IsSubmitted
138
  return attemptStatus, examEventAttempt, nil
 
236
  return entity.ExamEventAttempt{}, err
237
  }
238
  attemptStatus, examEventAttempt, err := s.GetExamEventAttempt(ctx, eventSlug, examSlug, accountId)
239
+
240
+ fmt.Println("Get AttemptStatus = ", attemptStatus, "Err =", err)
241
+
242
  if err != nil {
243
  return entity.ExamEventAttempt{}, err
244
  }
 
249
  if err != nil {
250
  return entity.ExamEventAttempt{}, err
251
  }
252
+
253
  if attemptStatus.IsNotAttempt {
254
 
255
  if eventStatus.IsFinished {
256
+ return entity.ExamEventAttempt{}, err
257
  }
258
 
259
  startTime, dueTime := s.SetupExamTimer(ctx, exam)
 
410
  return CPQuestionVerdict, err
411
  }
412
 
413
+
space/.gitignore CHANGED
@@ -1,2 +1,2 @@
1
- /.env
2
  go-boilerplate.exe
 
1
+ .env
2
  go-boilerplate.exe
utils/response_util.go CHANGED
@@ -59,7 +59,7 @@ func ResponseFAILED[TMetaData any](c *gin.Context, metaData TMetaData, err error
59
  MetaData: metaData,
60
  })
61
  return
62
- } else if errors.Is(err, http_error.FORBIDDEN_ERROR) {
63
  c.JSON(403, dto.ErrorResponse{
64
  Status: "error",
65
  Error: err,
@@ -70,8 +70,7 @@ func ResponseFAILED[TMetaData any](c *gin.Context, metaData TMetaData, err error
70
  } else if errors.Is(err, http_error.EVENT_START_DATE_IN_PAST) ||
71
  errors.Is(err, http_error.EVENT_START_DATE_INVALID) ||
72
  errors.Is(err, http_error.EVENT_END_DATE_INVALID) ||
73
- errors.Is(err, http_error.INVALID_DATE_FORMAT) ||
74
- errors.Is(err, http_error.INVALID_CODE) {
75
  c.JSON(400, dto.ErrorResponse{
76
  Status: "error",
77
  Error: err,
 
59
  MetaData: metaData,
60
  })
61
  return
62
+ } else if errors.Is(err, http_error.FORBIDDEN_ERROR) || errors.Is(err, http_error.INVALID_CODE) {
63
  c.JSON(403, dto.ErrorResponse{
64
  Status: "error",
65
  Error: err,
 
70
  } else if errors.Is(err, http_error.EVENT_START_DATE_IN_PAST) ||
71
  errors.Is(err, http_error.EVENT_START_DATE_INVALID) ||
72
  errors.Is(err, http_error.EVENT_END_DATE_INVALID) ||
73
+ errors.Is(err, http_error.INVALID_DATE_FORMAT) {
 
74
  c.JSON(400, dto.ErrorResponse{
75
  Status: "error",
76
  Error: err,
utils/utils.go CHANGED
@@ -3,6 +3,7 @@ package utils
3
  import (
4
  "time"
5
 
 
6
  http_error "abdanhafidz.com/go-boilerplate/models/error"
7
  "github.com/google/uuid"
8
  )
@@ -43,3 +44,11 @@ func TimePtrToString(t *time.Time) *string {
43
  s := t.Format(time.RFC3339)
44
  return &s
45
  }
 
 
 
 
 
 
 
 
 
3
  import (
4
  "time"
5
 
6
+ models "abdanhafidz.com/go-boilerplate/models/entity"
7
  http_error "abdanhafidz.com/go-boilerplate/models/error"
8
  "github.com/google/uuid"
9
  )
 
44
  s := t.Format(time.RFC3339)
45
  return &s
46
  }
47
+
48
+ func ValidateCode(code string) error {
49
+ if !models.CodeRegex.MatchString(code) {
50
+ return http_error.INVALID_CODE
51
+ }
52
+ return nil
53
+ }
54
+