lifedebugger commited on
Commit
6d32e58
·
1 Parent(s): 2d345d7

Deploy files from GitHub repository

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. controller/quiz/answer_quiz_controller.go +2 -2
  2. controller/quiz/question_quiz_controller.go +5 -3
  3. space/controller/quiz/answer_quiz_controller.go +2 -1
  4. space/space/controller/quiz/attempt_quiz_controller.go +2 -1
  5. space/space/controller/quiz/submit_quiz_controller.go +23 -0
  6. space/space/models/database_orm_model.go +6 -0
  7. space/space/models/response_model.go +5 -0
  8. space/space/repositories/quiz_repository.go +18 -0
  9. space/space/router/quiz_route.go +1 -0
  10. space/space/services/academy_quiz_service.go +30 -0
  11. space/space/space/config/database_connection_config.go +5 -0
  12. space/space/space/controller/quiz/answer_quiz_controller.go +24 -0
  13. space/space/space/controller/quiz/attempt_quiz_controller.go +24 -0
  14. space/space/space/controller/quiz/question_quiz_controller.go +25 -0
  15. space/space/space/models/database_orm_model.go +47 -0
  16. space/space/space/models/exception_model.go +3 -0
  17. space/space/space/models/request_model.go +9 -0
  18. space/space/space/models/response_model.go +11 -0
  19. space/space/space/repositories/academy_repository.go +11 -0
  20. space/space/space/repositories/question_repository.go +36 -0
  21. space/space/space/repositories/quiz_repository.go +101 -0
  22. space/space/space/router/quiz_route.go +16 -0
  23. space/space/space/router/router.go +1 -1
  24. space/space/space/services/academy_quiz_answer_service.go +56 -0
  25. space/space/space/services/academy_quiz_question_service.go +42 -0
  26. space/space/space/services/academy_quiz_service.go +124 -0
  27. space/space/space/space/controller/academy/academy_contents_controller.go +1 -1
  28. space/space/space/space/models/database_orm_model.go +5 -3
  29. space/space/space/space/models/response_model.go +1 -1
  30. space/space/space/space/repositories/academy_repository.go +4 -4
  31. space/space/space/space/services/academy_service.go +24 -9
  32. space/space/space/space/services/register_service.go +43 -3
  33. space/space/space/space/space/controller/academy/academy_contents_controller.go +19 -0
  34. space/space/space/space/space/controller/academy/academy_controller.go +1 -1
  35. space/space/space/space/space/controller/academy/academy_materials_controller.go +19 -0
  36. space/space/space/space/space/models/database_orm_model.go +8 -5
  37. space/space/space/space/space/repositories/academy_repository.go +11 -0
  38. space/space/space/space/space/router/academy_route.go +2 -1
  39. space/space/space/space/space/services/academy_service.go +36 -18
  40. space/space/space/space/space/services/jwt_service.go +1 -1
  41. space/space/space/space/space/services/service.go +3 -0
  42. space/space/space/space/space/space/services/email_verification_service.go +3 -14
  43. space/space/space/space/space/space/space/space/config/config.go +2 -0
  44. space/space/space/space/space/space/space/space/services/email_verification_service.go +100 -8
  45. space/space/space/space/space/space/space/space/space/services/external_authentication_service.go +10 -6
  46. space/space/space/space/space/space/space/space/space/space/services/email_verification_service.go +1 -4
  47. space/space/space/space/space/space/space/space/space/space/services/forgot_password_service.go +1 -4
  48. space/space/space/space/space/space/space/space/space/space/space/services/external_authentication_service.go +18 -1
  49. space/space/space/space/space/space/space/space/space/space/space/space/controller/academy/academy_controller.go +31 -0
  50. space/space/space/space/space/space/space/space/space/space/space/space/models/database_orm_model.go +7 -7
controller/quiz/answer_quiz_controller.go CHANGED
@@ -17,9 +17,9 @@ func Answer(c *gin.Context) {
17
  quizAnswerController.RequestJSON(c, func() {
18
  quizId, _ := strconv.Atoi(c.Param("quiz_id"))
19
  academyId, _ := strconv.Atoi(c.Param("academy_id"))
20
- questionNo, _ := strconv.Atoi(c.Query("question_no"))
21
  quizAnswerController.Service.Constructor.ID = uint(quizId)
22
  quizAnswerController.Service.Constructor.AcademyID = uint(academyId)
23
- quizAnswer.Update(quizAnswerController.AccountData.UserID, questionNo, quizAnswerController.Request.Answer)
24
  })
25
  }
 
17
  quizAnswerController.RequestJSON(c, func() {
18
  quizId, _ := strconv.Atoi(c.Param("quiz_id"))
19
  academyId, _ := strconv.Atoi(c.Param("academy_id"))
20
+
21
  quizAnswerController.Service.Constructor.ID = uint(quizId)
22
  quizAnswerController.Service.Constructor.AcademyID = uint(academyId)
23
+ quizAnswer.Update(quizAnswerController.AccountData.UserID, quizAnswerController.Request.QuestionNo, quizAnswerController.Request.Answer)
24
  })
25
  }
controller/quiz/question_quiz_controller.go CHANGED
@@ -11,15 +11,17 @@ import (
11
 
12
  func Question(c *gin.Context) {
13
  questionQuiz := services.QuestionQuizService{}
14
- questionQuizController := controller.Controller[models.QuestionQuizRequest, models.Quiz, models.QuestionResponse]{
15
  Service: &questionQuiz.Service,
16
  }
17
- questionQuizController.RequestJSON(c, func() {
18
  quizId, _ := strconv.Atoi(c.Param("quiz_id"))
19
  academyId, _ := strconv.Atoi(c.Param("academy_id"))
 
20
  questionQuizController.Service.Constructor.ID = uint(quizId)
21
  questionQuizController.Service.Constructor.AcademyID = uint(academyId)
22
- questionQuiz.Retrieve(questionQuizController.AccountData.UserID, questionQuizController.Request.QuestionNo)
 
23
 
24
  })
25
  }
 
11
 
12
  func Question(c *gin.Context) {
13
  questionQuiz := services.QuestionQuizService{}
14
+ questionQuizController := controller.Controller[any, models.Quiz, models.QuestionResponse]{
15
  Service: &questionQuiz.Service,
16
  }
17
+ questionQuizController.HeaderParse(c, func() {
18
  quizId, _ := strconv.Atoi(c.Param("quiz_id"))
19
  academyId, _ := strconv.Atoi(c.Param("academy_id"))
20
+ questionNo, _ := strconv.Atoi(c.Query("question_no"))
21
  questionQuizController.Service.Constructor.ID = uint(quizId)
22
  questionQuizController.Service.Constructor.AcademyID = uint(academyId)
23
+ questionQuiz.Retrieve(questionQuizController.AccountData.UserID, questionNo)
24
+ questionQuizController.Response(c)
25
 
26
  })
27
  }
space/controller/quiz/answer_quiz_controller.go CHANGED
@@ -17,8 +17,9 @@ func Answer(c *gin.Context) {
17
  quizAnswerController.RequestJSON(c, func() {
18
  quizId, _ := strconv.Atoi(c.Param("quiz_id"))
19
  academyId, _ := strconv.Atoi(c.Param("academy_id"))
 
20
  quizAnswerController.Service.Constructor.ID = uint(quizId)
21
  quizAnswerController.Service.Constructor.AcademyID = uint(academyId)
22
- quizAnswer.Update(quizAnswerController.AccountData.UserID, quizAnswerController.Request.QuestionNo, quizAnswerController.Request.Answer)
23
  })
24
  }
 
17
  quizAnswerController.RequestJSON(c, func() {
18
  quizId, _ := strconv.Atoi(c.Param("quiz_id"))
19
  academyId, _ := strconv.Atoi(c.Param("academy_id"))
20
+ questionNo, _ := strconv.Atoi(c.Query("question_no"))
21
  quizAnswerController.Service.Constructor.ID = uint(quizId)
22
  quizAnswerController.Service.Constructor.AcademyID = uint(academyId)
23
+ quizAnswer.Update(quizAnswerController.AccountData.UserID, questionNo, quizAnswerController.Request.Answer)
24
  })
25
  }
space/space/controller/quiz/attempt_quiz_controller.go CHANGED
@@ -14,11 +14,12 @@ func Attempt(c *gin.Context) {
14
  attemptQuizController := controller.Controller[any, models.Quiz, models.QuizAttempt]{
15
  Service: &attemptQuiz.Service,
16
  }
17
- attemptQuizController.RequestJSON(c, func() {
18
  quizId, _ := strconv.Atoi(c.Param("quiz_id"))
19
  academyId, _ := strconv.Atoi(c.Param("academy_id"))
20
  attemptQuizController.Service.Constructor.ID = uint(quizId)
21
  attemptQuizController.Service.Constructor.AcademyID = uint(academyId)
22
  attemptQuiz.Create(attemptQuizController.AccountData.UserID)
 
23
  })
24
  }
 
14
  attemptQuizController := controller.Controller[any, models.Quiz, models.QuizAttempt]{
15
  Service: &attemptQuiz.Service,
16
  }
17
+ attemptQuizController.HeaderParse(c, func() {
18
  quizId, _ := strconv.Atoi(c.Param("quiz_id"))
19
  academyId, _ := strconv.Atoi(c.Param("academy_id"))
20
  attemptQuizController.Service.Constructor.ID = uint(quizId)
21
  attemptQuizController.Service.Constructor.AcademyID = uint(academyId)
22
  attemptQuiz.Create(attemptQuizController.AccountData.UserID)
23
+ attemptQuizController.Response(c)
24
  })
25
  }
space/space/controller/quiz/submit_quiz_controller.go ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package controller
2
+
3
+ import (
4
+ "strconv"
5
+
6
+ "api.qobiltu.id/controller"
7
+ "api.qobiltu.id/models"
8
+ "api.qobiltu.id/services"
9
+ "github.com/gin-gonic/gin"
10
+ )
11
+
12
+ func Submit(c *gin.Context) {
13
+ submitQuiz := services.SubmitQuizService{}
14
+ submitQuizController := controller.Controller[any, models.QuizAttempt, models.QuizResultResponse]{
15
+ Service: &submitQuiz.Service,
16
+ }
17
+ submitQuizController.HeaderParse(c, func() {
18
+ quizId, _ := strconv.Atoi(c.Param("attempt_id"))
19
+ submitQuizController.Service.Constructor.ID = uint(quizId)
20
+ submitQuiz.Create(submitQuizController.AccountData.UserID)
21
+ submitQuizController.Response(c)
22
+ })
23
+ }
space/space/models/database_orm_model.go CHANGED
@@ -177,6 +177,12 @@ type UserAnswer struct {
177
  SelectedAnswer uint `json:"selected_answer"`
178
  IsCorrect bool `json:"is_correct"`
179
  }
 
 
 
 
 
 
180
 
181
  // Gorm table name settings
182
  func (Account) TableName() string { return "account" }
 
177
  SelectedAnswer uint `json:"selected_answer"`
178
  IsCorrect bool `json:"is_correct"`
179
  }
180
+ type QuizResult struct {
181
+ QuizAttemptID uint `gorm:"column:quiz_attempt_id"`
182
+ TotalQuestions int `gorm:"column:total_questions"`
183
+ CorrectAnswers int `gorm:"column:correct_answers"`
184
+ AverageScore float64 `gorm:"column:average_score"`
185
+ }
186
 
187
  // Gorm table name settings
188
  func (Account) TableName() string { return "account" }
space/space/models/response_model.go CHANGED
@@ -54,3 +54,8 @@ type QuestionResponse struct {
54
  Answer []Answer `json:"answer_options"`
55
  UserAnswer int `json:"current_user_answer"`
56
  }
 
 
 
 
 
 
54
  Answer []Answer `json:"answer_options"`
55
  UserAnswer int `json:"current_user_answer"`
56
  }
57
+
58
+ type QuizResultResponse struct {
59
+ QuizAttempt QuizAttempt `json:"quiz_attempt"`
60
+ Result QuizResult `json:"result"`
61
+ }
space/space/repositories/quiz_repository.go CHANGED
@@ -99,3 +99,21 @@ func UpdateUserAnswer(userAnswer models.UserAnswer) Repository[models.UserAnswer
99
  Update(repo)
100
  return *repo
101
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  Update(repo)
100
  return *repo
101
  }
102
+
103
+ func UpdateQuizAttempt(quizAttempt models.QuizAttempt) Repository[models.QuizAttempt, models.QuizAttempt] {
104
+ repo := Construct[models.QuizAttempt, models.QuizAttempt](
105
+ quizAttempt,
106
+ )
107
+ Update(repo)
108
+ return *repo
109
+ }
110
+
111
+ func CountUserAttemptScore(attemptId uint) Repository[models.QuizAttempt, models.QuizResult] {
112
+ repo := Construct[models.QuizAttempt, models.QuizResult](
113
+ models.QuizAttempt{ID: attemptId},
114
+ )
115
+ repo.Transaction.Model(&repo.Constructor).Raw("SELECT quiz_attempt_id,COUNT(*) AS total_questions,SUM(CASE WHEN is_correct = true THEN 1 ELSE 0 END) AS correct_answers,CAST(SUM(CASE WHEN is_correct = true THEN 1 ELSE 0 END) AS FLOAT) / COUNT(*) AS average_score FROM user_answers WHERE quiz_attempt_id = ? GROUP BY quiz_attempt_id", attemptId).Scan(&repo.Result)
116
+ repo.RowsError = repo.Transaction.Error
117
+ repo.NoRecord = true
118
+ return *repo
119
+ }
space/space/router/quiz_route.go CHANGED
@@ -12,5 +12,6 @@ func QuizRoute(router *gin.Engine) {
12
  routerGroup.POST("/:academy_id/:quiz_id/attempt", middleware.AuthUser, QuizController.Attempt)
13
  routerGroup.GET("/:academy_id/:quiz_id/question", middleware.AuthUser, QuizController.Question)
14
  routerGroup.PUT("/:academy_id/:quiz_id/choose-answer", middleware.AuthUser, QuizController.Answer)
 
15
  }
16
  }
 
12
  routerGroup.POST("/:academy_id/:quiz_id/attempt", middleware.AuthUser, QuizController.Attempt)
13
  routerGroup.GET("/:academy_id/:quiz_id/question", middleware.AuthUser, QuizController.Question)
14
  routerGroup.PUT("/:academy_id/:quiz_id/choose-answer", middleware.AuthUser, QuizController.Answer)
15
+ routerGroup.POST("/submit-attempt/:attempt_id", middleware.AuthUser, QuizController.Submit)
16
  }
17
  }
space/space/services/academy_quiz_service.go CHANGED
@@ -13,6 +13,10 @@ type AttemptQuizService struct {
13
  Service[models.Quiz, models.QuizAttempt]
14
  }
15
 
 
 
 
 
16
  func AuthorizeQuizwithAcademy(s *AttemptQuizService, next func()) {
17
  academyRepo := repositories.GetAcademyByID(s.Constructor.AcademyID)
18
  s.Error = academyRepo.RowsError
@@ -122,3 +126,29 @@ func (s *AttemptQuizService) Create(userID uint) {
122
  }
123
  })
124
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  Service[models.Quiz, models.QuizAttempt]
14
  }
15
 
16
+ type SubmitQuizService struct {
17
+ Service[models.QuizAttempt, models.QuizResultResponse]
18
+ }
19
+
20
  func AuthorizeQuizwithAcademy(s *AttemptQuizService, next func()) {
21
  academyRepo := repositories.GetAcademyByID(s.Constructor.AcademyID)
22
  s.Error = academyRepo.RowsError
 
126
  }
127
  })
128
  }
129
+ func (s *SubmitQuizService) Create(userID uint) {
130
+ quizAttemptRepo := repositories.GetAttemptById(s.Constructor.ID)
131
+ if quizAttemptRepo.NoRecord {
132
+ s.Exception.DataNotFound = true
133
+ s.Exception.Message = "There is no quiz attempt with given user!"
134
+ return
135
+ }
136
+ s.Error = errors.Join(s.Error, quizAttemptRepo.RowsError)
137
+ countScoreRepo := repositories.CountUserAttemptScore(s.Constructor.ID)
138
+ s.Error = errors.Join(s.Error, countScoreRepo.RowsError)
139
+
140
+ if quizAttemptRepo.Result.FinishedAt == nil {
141
+ finishTime := time.Now()
142
+ quizAttemptRepo.Result.FinishedAt = &finishTime
143
+ quizAttemptRepo.Result.Score = countScoreRepo.Result.AverageScore * 100
144
+ updateAttemptRepo := repositories.UpdateQuizAttempt(quizAttemptRepo.Result)
145
+ s.Error = errors.Join(s.Error, updateAttemptRepo.RowsError)
146
+ }
147
+
148
+ s.Result = models.QuizResultResponse{
149
+ QuizAttempt: quizAttemptRepo.Result,
150
+ Result: countScoreRepo.Result,
151
+ }
152
+
153
+ return
154
+ }
space/space/space/config/database_connection_config.go CHANGED
@@ -64,6 +64,11 @@ func AutoMigrateAll(db *gorm.DB) {
64
  &models.RegionProvince{},
65
  &models.OptionCategory{},
66
  &models.OptionValues{},
 
 
 
 
 
67
  )
68
 
69
  if err != nil {
 
64
  &models.RegionProvince{},
65
  &models.OptionCategory{},
66
  &models.OptionValues{},
67
+ &models.Quiz{},
68
+ &models.QuizAttempt{},
69
+ &models.Question{},
70
+ &models.Answer{},
71
+ &models.UserAnswer{},
72
  )
73
 
74
  if err != nil {
space/space/space/controller/quiz/answer_quiz_controller.go ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package controller
2
+
3
+ import (
4
+ "strconv"
5
+
6
+ "api.qobiltu.id/controller"
7
+ "api.qobiltu.id/models"
8
+ "api.qobiltu.id/services"
9
+ "github.com/gin-gonic/gin"
10
+ )
11
+
12
+ func Answer(c *gin.Context) {
13
+ quizAnswer := services.AnswerQuizService{}
14
+ quizAnswerController := controller.Controller[models.AnswerQuizRequest, models.Quiz, models.QuestionResponse]{
15
+ Service: &quizAnswer.Service,
16
+ }
17
+ quizAnswerController.RequestJSON(c, func() {
18
+ quizId, _ := strconv.Atoi(c.Param("quiz_id"))
19
+ academyId, _ := strconv.Atoi(c.Param("academy_id"))
20
+ quizAnswerController.Service.Constructor.ID = uint(quizId)
21
+ quizAnswerController.Service.Constructor.AcademyID = uint(academyId)
22
+ quizAnswer.Update(quizAnswerController.AccountData.UserID, quizAnswerController.Request.QuestionNo, quizAnswerController.Request.Answer)
23
+ })
24
+ }
space/space/space/controller/quiz/attempt_quiz_controller.go ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package controller
2
+
3
+ import (
4
+ "strconv"
5
+
6
+ "api.qobiltu.id/controller"
7
+ "api.qobiltu.id/models"
8
+ "api.qobiltu.id/services"
9
+ "github.com/gin-gonic/gin"
10
+ )
11
+
12
+ func Attempt(c *gin.Context) {
13
+ attemptQuiz := services.AttemptQuizService{}
14
+ attemptQuizController := controller.Controller[any, models.Quiz, models.QuizAttempt]{
15
+ Service: &attemptQuiz.Service,
16
+ }
17
+ attemptQuizController.RequestJSON(c, func() {
18
+ quizId, _ := strconv.Atoi(c.Param("quiz_id"))
19
+ academyId, _ := strconv.Atoi(c.Param("academy_id"))
20
+ attemptQuizController.Service.Constructor.ID = uint(quizId)
21
+ attemptQuizController.Service.Constructor.AcademyID = uint(academyId)
22
+ attemptQuiz.Create(attemptQuizController.AccountData.UserID)
23
+ })
24
+ }
space/space/space/controller/quiz/question_quiz_controller.go ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package controller
2
+
3
+ import (
4
+ "strconv"
5
+
6
+ "api.qobiltu.id/controller"
7
+ "api.qobiltu.id/models"
8
+ "api.qobiltu.id/services"
9
+ "github.com/gin-gonic/gin"
10
+ )
11
+
12
+ func Question(c *gin.Context) {
13
+ questionQuiz := services.QuestionQuizService{}
14
+ questionQuizController := controller.Controller[models.QuestionQuizRequest, models.Quiz, models.QuestionResponse]{
15
+ Service: &questionQuiz.Service,
16
+ }
17
+ questionQuizController.RequestJSON(c, func() {
18
+ quizId, _ := strconv.Atoi(c.Param("quiz_id"))
19
+ academyId, _ := strconv.Atoi(c.Param("academy_id"))
20
+ questionQuizController.Service.Constructor.ID = uint(quizId)
21
+ questionQuizController.Service.Constructor.AcademyID = uint(academyId)
22
+ questionQuiz.Retrieve(questionQuizController.AccountData.UserID, questionQuizController.Request.QuestionNo)
23
+
24
+ })
25
+ }
space/space/space/models/database_orm_model.go CHANGED
@@ -137,6 +137,46 @@ type RegionCity struct {
137
  FullCode string `json:"full_code"`
138
  ProvinceID uint `json:"province_id"`
139
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
 
141
  // Gorm table name settings
142
  func (Account) TableName() string { return "account" }
@@ -152,3 +192,10 @@ func (AcademyMaterialProgress) TableName() string { return "academy_materials_pr
152
  func (AcademyContentProgress) TableName() string { return "academy_contents_progress" }
153
  func (RegionProvince) TableName() string { return "region_provinces" }
154
  func (RegionCity) TableName() string { return "region_cities" }
 
 
 
 
 
 
 
 
137
  FullCode string `json:"full_code"`
138
  ProvinceID uint `json:"province_id"`
139
  }
140
+ type Answer struct {
141
+ ID uint `gorm:"primaryKey"`
142
+ QuestionID uint `json:"question_id"`
143
+ Content string `json:"content"`
144
+ IsCorrect bool `json:"-"`
145
+ }
146
+ type Question struct {
147
+ ID uint `gorm:"primaryKey"`
148
+ QuizID uint `json:"quiz_id"`
149
+ Content string `json:"content"`
150
+ Order int `json:"order"`
151
+ CorrectAnswer uint `json:"-"`
152
+ }
153
+ type Quiz struct {
154
+ ID uint `gorm:"primaryKey" json:"id"`
155
+ AcademyID uint `json:"academy_id"`
156
+ Title string `json:"title"`
157
+ Description string `json:"description"`
158
+ AttemptLimit int `json:"attempt_limit"`
159
+ TimeLimit int `json:"time_limit"`
160
+ MinScore int `json:"min_score"`
161
+ CreatedAt time.Time `json:"created_at"`
162
+ }
163
+
164
+ type QuizAttempt struct {
165
+ ID uint `gorm:"primaryKey"`
166
+ AccountID uint `json:"user_id"`
167
+ QuizID uint `json:"quiz_id"`
168
+ StartedAt time.Time `json:"started_at"`
169
+ DueAt time.Time `json:"due_at"`
170
+ FinishedAt *time.Time `json:"finished_at"`
171
+ Score float64 `json:"score"`
172
+ }
173
+ type UserAnswer struct {
174
+ ID uint `gorm:"primaryKey"`
175
+ QuizAttemptID uint `json:"quiz_attempt_id"`
176
+ QuestionID uint `json:"question_id"`
177
+ SelectedAnswer uint `json:"selected_answer"`
178
+ IsCorrect bool `json:"is_correct"`
179
+ }
180
 
181
  // Gorm table name settings
182
  func (Account) TableName() string { return "account" }
 
192
  func (AcademyContentProgress) TableName() string { return "academy_contents_progress" }
193
  func (RegionProvince) TableName() string { return "region_provinces" }
194
  func (RegionCity) TableName() string { return "region_cities" }
195
+ func (Answer) TableName() string { return "answers" }
196
+ func (Question) TableName() string { return "questions" }
197
+ func (Quiz) TableName() string { return "quizzes" }
198
+ func (QuizAttempt) TableName() string { return "quiz_attempts" }
199
+ func (UserAnswer) TableName() string { return "user_answers" }
200
+ func (OptionCategory) TableName() string { return "option_categories" }
201
+ func (OptionValues) TableName() string { return "option_values" }
space/space/space/models/exception_model.go CHANGED
@@ -8,5 +8,8 @@ type Exception struct {
8
  DataDuplicate bool `json:"data_duplicate,omitempty"`
9
  QueryError bool `json:"query_error,omitempty"`
10
  InvalidPasswordLength bool `json:"invalid_password_length,omitempty"`
 
 
 
11
  Message string `json:"message,omitempty"`
12
  }
 
8
  DataDuplicate bool `json:"data_duplicate,omitempty"`
9
  QueryError bool `json:"query_error,omitempty"`
10
  InvalidPasswordLength bool `json:"invalid_password_length,omitempty"`
11
+ IsPassTheLimit bool `json:"is_pass_the_limit,omitempty"`
12
+ IsTimeOut bool `json:"is_time_out,omitempty"`
13
+ AttemptNotFound bool `json:"attempt_not_found,omitempty"`
14
  Message string `json:"message,omitempty"`
15
  }
space/space/space/models/request_model.go CHANGED
@@ -40,3 +40,12 @@ type ValidateForgotPasswordRequest struct {
40
  Token uint `json:"token" binding:"required"`
41
  NewPassword string `json:"new_password"`
42
  }
 
 
 
 
 
 
 
 
 
 
40
  Token uint `json:"token" binding:"required"`
41
  NewPassword string `json:"new_password"`
42
  }
43
+
44
+ type QuestionQuizRequest struct {
45
+ QuestionNo int `json:"question_no" binding:"required"`
46
+ }
47
+
48
+ type AnswerQuizRequest struct {
49
+ QuestionNo int `json:"question_no" binding:"required"`
50
+ Answer int `json:"answer" binding:"required"`
51
+ }
space/space/space/models/response_model.go CHANGED
@@ -43,3 +43,14 @@ type AcademyResponse struct {
43
  type AllAcademyResponse struct {
44
  Academies []AcademyResponse `json:"academy_dasar"`
45
  }
 
 
 
 
 
 
 
 
 
 
 
 
43
  type AllAcademyResponse struct {
44
  Academies []AcademyResponse `json:"academy_dasar"`
45
  }
46
+
47
+ type AttemptExamResponse struct {
48
+ Exam Quiz `json:"exam"`
49
+ Questions []Question `json:"questions"`
50
+ }
51
+
52
+ type QuestionResponse struct {
53
+ Question Question `json:"question"`
54
+ Answer []Answer `json:"answer_options"`
55
+ UserAnswer int `json:"current_user_answer"`
56
+ }
space/space/space/repositories/academy_repository.go CHANGED
@@ -82,3 +82,14 @@ func GetAcademyMaterialBySlug(slug string) Repository[models.AcademyMaterial, mo
82
  )
83
  return *repo
84
  }
 
 
 
 
 
 
 
 
 
 
 
 
82
  )
83
  return *repo
84
  }
85
+
86
+ func GetAcademyByID(id uint) Repository[models.Academy, models.Academy] {
87
+ repo := Construct[models.Academy, models.Academy](
88
+ models.Academy{ID: id},
89
+ )
90
+ repo.Transactions(
91
+ WhereGivenConstructor[models.Academy, models.Academy],
92
+ Find[models.Academy, models.Academy],
93
+ )
94
+ return *repo
95
+ }
space/space/space/repositories/question_repository.go ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package repositories
2
+
3
+ import "api.qobiltu.id/models"
4
+
5
+ func GetQuestionByQuizId(quizId uint) Repository[models.Question, []models.Question] {
6
+ repo := Construct[models.Question, []models.Question](
7
+ models.Question{QuizID: quizId},
8
+ )
9
+ repo.Transactions(
10
+ WhereGivenConstructor[models.Question, []models.Question],
11
+ Find[models.Question, []models.Question],
12
+ )
13
+ return *repo
14
+ }
15
+
16
+ func GetQuestionByOrder(quizId uint, order int) Repository[models.Question, models.Question] {
17
+ repo := Construct[models.Question, models.Question](
18
+ models.Question{QuizID: quizId, Order: order},
19
+ )
20
+ repo.Transactions(
21
+ WhereGivenConstructor[models.Question, models.Question],
22
+ Find[models.Question, models.Question],
23
+ )
24
+ return *repo
25
+ }
26
+
27
+ func GetAnswerByQuestionId(questionId uint) Repository[models.Answer, []models.Answer] {
28
+ repo := Construct[models.Answer, []models.Answer](
29
+ models.Answer{QuestionID: questionId},
30
+ )
31
+ repo.Transactions(
32
+ WhereGivenConstructor[models.Answer, []models.Answer],
33
+ Find[models.Answer, []models.Answer],
34
+ )
35
+ return *repo
36
+ }
space/space/space/repositories/quiz_repository.go ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package repositories
2
+
3
+ import (
4
+ "api.qobiltu.id/models"
5
+ )
6
+
7
+ func GetQuizbyAcademyId(academyId uint) Repository[models.Quiz, []models.Quiz] {
8
+ repo := Construct[models.Quiz, []models.Quiz](
9
+ models.Quiz{AcademyID: academyId},
10
+ )
11
+ repo.Transactions(
12
+ WhereGivenConstructor[models.Quiz, []models.Quiz],
13
+ Find[models.Quiz, []models.Quiz],
14
+ )
15
+ return *repo
16
+ }
17
+
18
+ func GetAllUserAttempt(userId uint, quizId uint) Repository[models.QuizAttempt, []models.QuizAttempt] {
19
+ repo := Construct[models.QuizAttempt, []models.QuizAttempt](
20
+ models.QuizAttempt{AccountID: userId, QuizID: quizId},
21
+ )
22
+ repo.Transactions(
23
+ WhereGivenConstructor[models.QuizAttempt, []models.QuizAttempt],
24
+ Find[models.QuizAttempt, []models.QuizAttempt],
25
+ )
26
+ return *repo
27
+ }
28
+
29
+ func GetQuizbyId(quizId uint) Repository[models.Quiz, models.Quiz] {
30
+ repo := Construct[models.Quiz, models.Quiz](
31
+ models.Quiz{ID: quizId},
32
+ )
33
+ repo.Transactions(
34
+ WhereGivenConstructor[models.Quiz, models.Quiz],
35
+ Find[models.Quiz, models.Quiz],
36
+ )
37
+
38
+ return *repo
39
+ }
40
+
41
+ func GetUserLastAttempt(userId uint, quizId uint) Repository[models.QuizAttempt, models.QuizAttempt] {
42
+ repo := Construct[models.QuizAttempt, models.QuizAttempt](
43
+ models.QuizAttempt{AccountID: userId, QuizID: quizId},
44
+ )
45
+ repo.Transaction.Where(&repo.Constructor).Last(&repo.Result)
46
+ repo.RowsError = repo.Transaction.Error
47
+ repo.NoRecord = false
48
+ // fmt.Println(repo.Transaction.RowsAffected) Kenapa 0 !!!!
49
+ return *repo
50
+ }
51
+
52
+ func GetAttemptById(attemptId uint) Repository[models.QuizAttempt, models.QuizAttempt] {
53
+ repo := Construct[models.QuizAttempt, models.QuizAttempt](
54
+ models.QuizAttempt{ID: attemptId},
55
+ )
56
+ repo.Transactions(
57
+ WhereGivenConstructor[models.QuizAttempt, models.QuizAttempt],
58
+ Find[models.QuizAttempt, models.QuizAttempt],
59
+ )
60
+ return *repo
61
+ }
62
+
63
+ func CreateAttempt(quizAttempt models.QuizAttempt) Repository[models.QuizAttempt, models.QuizAttempt] {
64
+ repo := Construct[models.QuizAttempt, models.QuizAttempt](
65
+ quizAttempt,
66
+ )
67
+
68
+ Create(repo)
69
+ return *repo
70
+ }
71
+
72
+ func GetUserAnswerByAttemptQuestionId(attemptId uint, questionId uint) Repository[models.UserAnswer, models.UserAnswer] {
73
+ repo := Construct[models.UserAnswer, models.UserAnswer](
74
+ models.UserAnswer{
75
+ QuizAttemptID: attemptId,
76
+ QuestionID: questionId,
77
+ },
78
+ )
79
+ repo.Transactions(
80
+ WhereGivenConstructor[models.UserAnswer, models.UserAnswer],
81
+ Find[models.UserAnswer, models.UserAnswer],
82
+ )
83
+ return *repo
84
+ }
85
+
86
+ func CreateUserAnswer(userAnswer models.UserAnswer) Repository[models.UserAnswer, models.UserAnswer] {
87
+ repo := Construct[models.UserAnswer, models.UserAnswer](
88
+ userAnswer,
89
+ )
90
+
91
+ Create(repo)
92
+ return *repo
93
+ }
94
+
95
+ func UpdateUserAnswer(userAnswer models.UserAnswer) Repository[models.UserAnswer, models.UserAnswer] {
96
+ repo := Construct[models.UserAnswer, models.UserAnswer](
97
+ userAnswer,
98
+ )
99
+ Update(repo)
100
+ return *repo
101
+ }
space/space/space/router/quiz_route.go ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package router
2
+
3
+ import (
4
+ QuizController "api.qobiltu.id/controller/quiz"
5
+ "api.qobiltu.id/middleware"
6
+ "github.com/gin-gonic/gin"
7
+ )
8
+
9
+ func QuizRoute(router *gin.Engine) {
10
+ routerGroup := router.Group("/api/v1/quiz")
11
+ {
12
+ routerGroup.POST("/:academy_id/:quiz_id/attempt", middleware.AuthUser, QuizController.Attempt)
13
+ routerGroup.GET("/:academy_id/:quiz_id/question", middleware.AuthUser, QuizController.Question)
14
+ routerGroup.PUT("/:academy_id/:quiz_id/choose-answer", middleware.AuthUser, QuizController.Answer)
15
+ }
16
+ }
space/space/space/router/router.go CHANGED
@@ -11,12 +11,12 @@ import (
11
  func StartService() {
12
  router := gin.Default()
13
  router.GET("/", controller.HomeController)
14
-
15
  AuthRoute(router)
16
  UserRoute(router)
17
  EmailRoute(router)
18
  OptionsRoute(router)
19
  AcademyRoute(router)
 
20
  err := router.Run(config.TCP_ADDRESS)
21
  if err != nil {
22
  log.Fatalf("Failed to run server: %v", err)
 
11
  func StartService() {
12
  router := gin.Default()
13
  router.GET("/", controller.HomeController)
 
14
  AuthRoute(router)
15
  UserRoute(router)
16
  EmailRoute(router)
17
  OptionsRoute(router)
18
  AcademyRoute(router)
19
+ QuizRoute(router)
20
  err := router.Run(config.TCP_ADDRESS)
21
  if err != nil {
22
  log.Fatalf("Failed to run server: %v", err)
space/space/space/services/academy_quiz_answer_service.go ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package services
2
+
3
+ import (
4
+ "errors"
5
+
6
+ "api.qobiltu.id/models"
7
+ "api.qobiltu.id/repositories"
8
+ )
9
+
10
+ type AnswerQuizService struct {
11
+ Service[models.Quiz, models.QuestionResponse]
12
+ }
13
+
14
+ func (s *AnswerQuizService) Update(userID uint, questionNo int, answer int) {
15
+ QuizAttemptService := AttemptQuizService{}
16
+ QuizAttemptService.Constructor = s.Constructor
17
+ QuizAttemptService.Validate(userID, func(latestAttemptRepo repositories.Repository[models.QuizAttempt, models.QuizAttempt], quizRepo repositories.Repository[models.Quiz, models.Quiz]) {
18
+
19
+ questionRepo := repositories.GetQuestionByOrder(latestAttemptRepo.Result.QuizID, questionNo)
20
+ if questionRepo.NoRecord {
21
+ s.Exception.DataNotFound = true
22
+ s.Exception.Message = "There is no quiz with given academy!"
23
+ return
24
+ }
25
+
26
+ s.Error = questionRepo.RowsError
27
+ answerRepo := repositories.GetUserAnswerByAttemptQuestionId(latestAttemptRepo.Result.ID, questionRepo.Result.ID)
28
+ if answerRepo.NoRecord {
29
+ s.Exception.DataNotFound = true
30
+ s.Exception.Message = "There is no Answer with given AttemptId!"
31
+ return
32
+ }
33
+ s.Error = errors.Join(s.Error, answerRepo.RowsError)
34
+
35
+ answerRepo.Result.SelectedAnswer = uint(answer)
36
+ answerRepo.Result.IsCorrect = (questionRepo.Result.CorrectAnswer == uint(answer))
37
+
38
+ updatedAnswer := repositories.UpdateUserAnswer(answerRepo.Result)
39
+
40
+ s.Error = errors.Join(s.Error, updatedAnswer.RowsError)
41
+
42
+ questionRepo.Result.CorrectAnswer = uint(0)
43
+ answerOptionRepo := repositories.GetAnswerByQuestionId(questionRepo.Result.ID)
44
+ if answerOptionRepo.NoRecord {
45
+ s.Exception.DataNotFound = true
46
+ s.Exception.Message = "There is no Answer Option with given QuestionId!"
47
+ return
48
+ }
49
+ s.Result = models.QuestionResponse{
50
+ Question: questionRepo.Result,
51
+ Answer: answerOptionRepo.Result,
52
+ UserAnswer: int(answerRepo.Result.SelectedAnswer),
53
+ }
54
+ return
55
+ })
56
+ }
space/space/space/services/academy_quiz_question_service.go ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package services
2
+
3
+ import (
4
+ "api.qobiltu.id/models"
5
+ "api.qobiltu.id/repositories"
6
+ )
7
+
8
+ type QuestionQuizService struct {
9
+ Service[models.Quiz, models.QuestionResponse]
10
+ }
11
+
12
+ func (s *QuestionQuizService) Retrieve(userID uint, questionNo int) {
13
+ QuizAttemptService := AttemptQuizService{}
14
+ QuizAttemptService.Constructor = s.Constructor
15
+ QuizAttemptService.Validate(userID, func(latestAttemptRepo repositories.Repository[models.QuizAttempt, models.QuizAttempt], quizRepo repositories.Repository[models.Quiz, models.Quiz]) {
16
+ questionRepo := repositories.GetQuestionByOrder(s.Constructor.ID, questionNo)
17
+ s.Error = questionRepo.RowsError
18
+ if questionRepo.NoRecord {
19
+ s.Exception.DataNotFound = true
20
+ s.Exception.Message = "There is no quiz with given academy!"
21
+ return
22
+ }
23
+ answerRepo := repositories.GetUserAnswerByAttemptQuestionId(latestAttemptRepo.Result.ID, questionRepo.Result.ID)
24
+ if answerRepo.NoRecord {
25
+ s.Exception.DataNotFound = true
26
+ s.Exception.Message = "There is no Answer with given AttemptId!"
27
+ return
28
+ }
29
+ answerOptionRepo := repositories.GetAnswerByQuestionId(questionRepo.Result.ID)
30
+ if answerOptionRepo.NoRecord {
31
+ s.Exception.DataNotFound = true
32
+ s.Exception.Message = "There is no Answer Option with given QuestionId!"
33
+ return
34
+ }
35
+ s.Result = models.QuestionResponse{
36
+ Question: questionRepo.Result,
37
+ Answer: answerOptionRepo.Result,
38
+ UserAnswer: int(answerRepo.Result.SelectedAnswer),
39
+ }
40
+ return
41
+ })
42
+ }
space/space/space/services/academy_quiz_service.go ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package services
2
+
3
+ import (
4
+ "errors"
5
+ "fmt"
6
+ "time"
7
+
8
+ "api.qobiltu.id/models"
9
+ "api.qobiltu.id/repositories"
10
+ )
11
+
12
+ type AttemptQuizService struct {
13
+ Service[models.Quiz, models.QuizAttempt]
14
+ }
15
+
16
+ func AuthorizeQuizwithAcademy(s *AttemptQuizService, next func()) {
17
+ academyRepo := repositories.GetAcademyByID(s.Constructor.AcademyID)
18
+ s.Error = academyRepo.RowsError
19
+ if academyRepo.NoRecord {
20
+ s.Exception.DataNotFound = true
21
+ s.Exception.Message = "There is no academy with given slug!"
22
+ return
23
+ }
24
+ quizRepo := repositories.GetQuizbyAcademyId(academyRepo.Result.ID)
25
+ s.Error = quizRepo.RowsError
26
+ if quizRepo.NoRecord {
27
+ s.Exception.DataNotFound = true
28
+ s.Exception.Message = "There is no quiz with given academy!"
29
+ return
30
+ }
31
+ next()
32
+ }
33
+ func CheckUserAttemptLimit(s *AttemptQuizService, allAttemptsRepo repositories.Repository[models.QuizAttempt, []models.QuizAttempt], quizRepo repositories.Repository[models.Quiz, models.Quiz], userID uint, next func()) {
34
+ if (allAttemptsRepo.RowsCount >= quizRepo.Result.AttemptLimit) && (allAttemptsRepo.Result[allAttemptsRepo.RowsCount-1].FinishedAt != nil) {
35
+ s.Exception.IsPassTheLimit = true
36
+ s.Exception.Message = "You have reached the attempt limit!"
37
+ return
38
+ }
39
+ next()
40
+ }
41
+
42
+ func CheckUserLatestAttempt(s *AttemptQuizService, latestAttemptRepo repositories.Repository[models.QuizAttempt, models.QuizAttempt], quizRepo repositories.Repository[models.Quiz, models.Quiz], userID uint, next func()) {
43
+ currentTime := time.Now()
44
+ if currentTime.After(latestAttemptRepo.Result.DueAt) {
45
+ s.Exception.IsTimeOut = true
46
+ s.Exception.Message = "Your latest attempt is timeout!"
47
+ // Submit
48
+ return
49
+ }
50
+ next()
51
+ }
52
+
53
+ func CheckUserAttemptable(s *AttemptQuizService, latestAttemptRepo repositories.Repository[models.QuizAttempt, []models.QuizAttempt], quizRepo repositories.Repository[models.Quiz, models.Quiz], userID uint, next func()) {
54
+ s.Error = errors.Join(s.Error, latestAttemptRepo.RowsError, quizRepo.RowsError)
55
+ if latestAttemptRepo.Result[latestAttemptRepo.RowsCount-1].FinishedAt != nil {
56
+ s.Exception.IsPassTheLimit = true
57
+ s.Exception.Message = "You have reached the attempt limit!"
58
+ return
59
+ }
60
+ next()
61
+ }
62
+ func (s *AttemptQuizService) Validate(userID uint, next func(latestAttemptRepo repositories.Repository[models.QuizAttempt, models.QuizAttempt], quizRepo repositories.Repository[models.Quiz, models.Quiz])) {
63
+ AuthorizeQuizwithAcademy(s, func() {
64
+ quizRepo := repositories.GetQuizbyId(s.Constructor.ID)
65
+ if quizRepo.NoRecord {
66
+ s.Exception.DataNotFound = true
67
+ s.Exception.Message = "There is no quiz data with given Id!"
68
+ return
69
+ }
70
+ allAttemptsRepo := repositories.GetAllUserAttempt(userID, s.Constructor.ID)
71
+ if allAttemptsRepo.NoRecord {
72
+ Attempt(s, quizRepo, userID)
73
+ allAttemptsRepo = repositories.GetAllUserAttempt(userID, s.Constructor.ID)
74
+ }
75
+ s.Error = errors.Join(allAttemptsRepo.RowsError, quizRepo.RowsError)
76
+ CheckUserAttemptLimit(s, allAttemptsRepo, quizRepo, userID, func() {
77
+ fmt.Println("accountID", userID)
78
+ fmt.Println("quizID", s.Constructor.ID)
79
+ latestAttemptRepo := repositories.GetUserLastAttempt(userID, s.Constructor.ID)
80
+ if latestAttemptRepo.NoRecord {
81
+ s.Exception.DataNotFound = true
82
+ s.Exception.Message = "There is no quiz attempt with given user!"
83
+ return
84
+ }
85
+ s.Error = errors.Join(s.Error, latestAttemptRepo.RowsError)
86
+ CheckUserLatestAttempt(s, latestAttemptRepo, quizRepo, userID, func() {
87
+ next(latestAttemptRepo, quizRepo)
88
+ })
89
+ })
90
+ })
91
+ }
92
+
93
+ func Attempt(s *AttemptQuizService, quizRepo repositories.Repository[models.Quiz, models.Quiz], userID uint) {
94
+ startTime := time.Now()
95
+ dueTime := startTime.Add(time.Duration(quizRepo.Result.TimeLimit) * time.Minute)
96
+ createdAttemptRepo := repositories.CreateAttempt(models.QuizAttempt{
97
+ AccountID: userID,
98
+ QuizID: s.Constructor.ID,
99
+ StartedAt: startTime,
100
+ DueAt: dueTime,
101
+ })
102
+ s.Error = createdAttemptRepo.RowsError
103
+ questionsRepo := repositories.GetQuestionByQuizId(s.Constructor.ID)
104
+ for _, question := range questionsRepo.Result {
105
+ createdUserAnswer := repositories.CreateUserAnswer(models.UserAnswer{
106
+ QuizAttemptID: createdAttemptRepo.Result.ID,
107
+ QuestionID: question.ID,
108
+ })
109
+ if createdUserAnswer.RowsError != nil {
110
+ s.Error = createdUserAnswer.RowsError
111
+ return
112
+ }
113
+ }
114
+ s.Result = createdAttemptRepo.Result
115
+ }
116
+ func (s *AttemptQuizService) Create(userID uint) {
117
+ s.Validate(userID, func(latestAttemptRepo repositories.Repository[models.QuizAttempt, models.QuizAttempt], quizRepo repositories.Repository[models.Quiz, models.Quiz]) {
118
+ if latestAttemptRepo.Result.FinishedAt != nil {
119
+ Attempt(s, quizRepo, userID)
120
+ } else {
121
+ s.Result = latestAttemptRepo.Result
122
+ }
123
+ })
124
+ }
space/space/space/space/controller/academy/academy_contents_controller.go CHANGED
@@ -9,7 +9,7 @@ import (
9
 
10
  func ContentList(c *gin.Context) {
11
  academyContent := services.AcademyContentService{}
12
- academyController := controller.Controller[any, models.AcademyMaterial, []models.AcademyContent]{
13
  Service: &academyContent.Service,
14
  }
15
  academyController.Service.Constructor.Slug = c.Param("slug_material")
 
9
 
10
  func ContentList(c *gin.Context) {
11
  academyContent := services.AcademyContentService{}
12
+ academyController := controller.Controller[any, models.AcademyMaterial, models.AcademyContent]{
13
  Service: &academyContent.Service,
14
  }
15
  academyController.Service.Constructor.Slug = c.Param("slug_material")
space/space/space/space/models/database_orm_model.go CHANGED
@@ -71,9 +71,11 @@ type Academy struct {
71
  ID uint `gorm:"primaryKey" json:"id"`
72
  UUID uuid.UUID `gorm:"type:uuid" json:"uuid"`
73
  Title string `json:"title"`
74
- Slug string `json:"slug"`
75
  TotalMaterial int `json:"total_material"`
76
  CompletedMaterial int `json:"completed_material"`
 
 
77
  Description string `json:"description"`
78
  }
79
 
@@ -82,7 +84,7 @@ type AcademyMaterial struct {
82
  UUID uuid.UUID `gorm:"type:uuid" json:"uuid"`
83
  AcademyID uint `json:"academy_id"`
84
  Title string `json:"title"`
85
- Slug string `json:"slug"`
86
  IsCompleted bool `json:"is_completed"`
87
  Description string `json:"description"`
88
  }
@@ -98,7 +100,7 @@ type AcademyContent struct {
98
  type OptionCategory struct {
99
  ID uint `gorm:"primaryKey" json:"id"`
100
  OptionName string `json:"option_name"`
101
- OptionSlug string `json:"option_slug"`
102
  }
103
 
104
  type OptionValues struct {
 
71
  ID uint `gorm:"primaryKey" json:"id"`
72
  UUID uuid.UUID `gorm:"type:uuid" json:"uuid"`
73
  Title string `json:"title"`
74
+ Slug string `json:"slug" gorm:"uniqueIndex" `
75
  TotalMaterial int `json:"total_material"`
76
  CompletedMaterial int `json:"completed_material"`
77
+ IsCompletedRead bool `json:"is_read"`
78
+ IsPassedExam bool `json:"is_exam"`
79
  Description string `json:"description"`
80
  }
81
 
 
84
  UUID uuid.UUID `gorm:"type:uuid" json:"uuid"`
85
  AcademyID uint `json:"academy_id"`
86
  Title string `json:"title"`
87
+ Slug string `json:"slug" gorm:"uniqueIndex"`
88
  IsCompleted bool `json:"is_completed"`
89
  Description string `json:"description"`
90
  }
 
100
  type OptionCategory struct {
101
  ID uint `gorm:"primaryKey" json:"id"`
102
  OptionName string `json:"option_name"`
103
+ OptionSlug string `json:"option_slug" gorm:"uniqueIndex"`
104
  }
105
 
106
  type OptionValues struct {
space/space/space/space/models/response_model.go CHANGED
@@ -33,7 +33,7 @@ type UserProfileResponse struct {
33
 
34
  type AcademyMaterialResponse struct {
35
  Materials AcademyMaterial
36
- Contents []AcademyContent
37
  }
38
  type AcademyResponse struct {
39
  Academy Academy `json:"academy"`
 
33
 
34
  type AcademyMaterialResponse struct {
35
  Materials AcademyMaterial
36
+ Contents AcademyContent
37
  }
38
  type AcademyResponse struct {
39
  Academy Academy `json:"academy"`
space/space/space/space/repositories/academy_repository.go CHANGED
@@ -35,13 +35,13 @@ func GetAllAcademyMaterialsByAcademyID(acaddemyId uint) Repository[models.Academ
35
  return *repo
36
  }
37
 
38
- func GetAllAcademyContentsByMaterialID(materialId uint) Repository[models.AcademyContent, []models.AcademyContent] {
39
- repo := Construct[models.AcademyContent, []models.AcademyContent](
40
  models.AcademyContent{AcademyMaterialID: materialId},
41
  )
42
  repo.Transactions(
43
- WhereGivenConstructor[models.AcademyContent, []models.AcademyContent],
44
- Find[models.AcademyContent, []models.AcademyContent],
45
  )
46
  return *repo
47
  }
 
35
  return *repo
36
  }
37
 
38
+ func GetAllAcademyContentsByMaterialID(materialId uint) Repository[models.AcademyContent, models.AcademyContent] {
39
+ repo := Construct[models.AcademyContent, models.AcademyContent](
40
  models.AcademyContent{AcademyMaterialID: materialId},
41
  )
42
  repo.Transactions(
43
+ WhereGivenConstructor[models.AcademyContent, models.AcademyContent],
44
+ Find[models.AcademyContent, models.AcademyContent],
45
  )
46
  return *repo
47
  }
space/space/space/space/services/academy_service.go CHANGED
@@ -1,6 +1,8 @@
1
  package services
2
 
3
  import (
 
 
4
  "api.qobiltu.id/models"
5
  "api.qobiltu.id/repositories"
6
  "github.com/gosimple/slug"
@@ -20,12 +22,13 @@ type AcademyMaterialService struct {
20
  }
21
 
22
  type AcademyContentService struct {
23
- Service[models.AcademyMaterial, []models.AcademyContent]
24
  }
25
 
26
  func castAcademyMaterials(academyId uint) []models.AcademyMaterialResponse {
27
  var ArrMaterials []models.AcademyMaterialResponse
28
- for _, academyMaterial := range repositories.GetAllAcademyMaterialsByAcademyID(academyId).Result {
 
29
  ArrMaterials = append(ArrMaterials, models.AcademyMaterialResponse{
30
  Materials: academyMaterial,
31
  Contents: repositories.GetAllAcademyContentsByMaterialID(academyMaterial.ID).Result,
@@ -78,7 +81,7 @@ func (s *CreateAcademyService) Create() {
78
  s.Error = createdMaterial.RowsError
79
  return
80
  }
81
- for _, content := range material.Contents {
82
  content.UUID = uuid.NewV4()
83
  content.AcademyMaterialID = createdMaterial.Result.ID
84
  createdContent := repositories.CreateAcademyContent(content)
@@ -98,9 +101,15 @@ func (s *CreateAcademyService) Create() {
98
  }
99
 
100
  func (s *AcademyMaterialService) Retrieve() {
101
- academyId := repositories.GetAcademyDataBySlug(s.Constructor.Slug).Result.ID
102
- academyMaterialRepo := repositories.GetAllAcademyMaterialsByAcademyID(academyId)
103
- s.Error = academyMaterialRepo.RowsError
 
 
 
 
 
 
104
  if academyMaterialRepo.NoRecord {
105
  s.Exception.DataNotFound = true
106
  s.Exception.Message = "There is no Academy Material with given ID"
@@ -110,9 +119,15 @@ func (s *AcademyMaterialService) Retrieve() {
110
  }
111
 
112
  func (s *AcademyContentService) Retrieve() {
113
- academyMaterialId := repositories.GetAcademyMaterialBySlug(s.Constructor.Slug).Result.ID
114
- academyContentRepo := repositories.GetAllAcademyContentsByMaterialID(academyMaterialId)
115
- s.Error = academyContentRepo.RowsError
 
 
 
 
 
 
116
  if academyContentRepo.NoRecord {
117
  s.Exception.DataNotFound = true
118
  s.Exception.Message = "There is no Academy Contents with given Material ID"
 
1
  package services
2
 
3
  import (
4
+ "errors"
5
+
6
  "api.qobiltu.id/models"
7
  "api.qobiltu.id/repositories"
8
  "github.com/gosimple/slug"
 
22
  }
23
 
24
  type AcademyContentService struct {
25
+ Service[models.AcademyMaterial, models.AcademyContent]
26
  }
27
 
28
  func castAcademyMaterials(academyId uint) []models.AcademyMaterialResponse {
29
  var ArrMaterials []models.AcademyMaterialResponse
30
+ academyMaterialsRepo := repositories.GetAllAcademyMaterialsByAcademyID(academyId)
31
+ for _, academyMaterial := range academyMaterialsRepo.Result {
32
  ArrMaterials = append(ArrMaterials, models.AcademyMaterialResponse{
33
  Materials: academyMaterial,
34
  Contents: repositories.GetAllAcademyContentsByMaterialID(academyMaterial.ID).Result,
 
81
  s.Error = createdMaterial.RowsError
82
  return
83
  }
84
+ for _, content := range []models.AcademyContent{material.Contents} {
85
  content.UUID = uuid.NewV4()
86
  content.AcademyMaterialID = createdMaterial.Result.ID
87
  createdContent := repositories.CreateAcademyContent(content)
 
101
  }
102
 
103
  func (s *AcademyMaterialService) Retrieve() {
104
+ academyRepo := repositories.GetAcademyDataBySlug(s.Constructor.Slug)
105
+ if academyRepo.NoRecord {
106
+ s.Exception.DataNotFound = true
107
+ s.Exception.Message = "There is no Academy found with given Slug!"
108
+ return
109
+ }
110
+ s.Error = academyRepo.RowsError
111
+ academyMaterialRepo := repositories.GetAllAcademyMaterialsByAcademyID(academyRepo.Result.ID)
112
+ s.Error = errors.Join(s.Error, academyMaterialRepo.RowsError)
113
  if academyMaterialRepo.NoRecord {
114
  s.Exception.DataNotFound = true
115
  s.Exception.Message = "There is no Academy Material with given ID"
 
119
  }
120
 
121
  func (s *AcademyContentService) Retrieve() {
122
+ academyMaterialRepo := repositories.GetAcademyMaterialBySlug(s.Constructor.Slug)
123
+ s.Error = academyMaterialRepo.RowsError
124
+ if academyMaterialRepo.NoRecord {
125
+ s.Exception.DataNotFound = true
126
+ s.Exception.Message = "There is no Academy Material with given Slug"
127
+ return
128
+ }
129
+ academyContentRepo := repositories.GetAllAcademyContentsByMaterialID(academyMaterialRepo.Result.ID)
130
+ s.Error = errors.Join(s.Error, academyContentRepo.RowsError)
131
  if academyContentRepo.NoRecord {
132
  s.Exception.DataNotFound = true
133
  s.Exception.Message = "There is no Academy Contents with given Material ID"
space/space/space/space/services/register_service.go CHANGED
@@ -2,6 +2,7 @@ package services
2
 
3
  import (
4
  "errors"
 
5
 
6
  "api.qobiltu.id/models"
7
  "api.qobiltu.id/repositories"
@@ -13,10 +14,49 @@ type RegisterService struct {
13
  Service[models.Account, models.Account]
14
  }
15
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  func (s *RegisterService) Create() {
17
- if len(s.Constructor.Password) < 8 {
18
- s.Exception.InvalidPasswordLength = true
19
- s.Exception.Message = "Password must have at least 8 characters!"
20
  return
21
  }
22
  hashed_password, err_hash := HashPassword(s.Constructor.Password)
 
2
 
3
  import (
4
  "errors"
5
+ "unicode"
6
 
7
  "api.qobiltu.id/models"
8
  "api.qobiltu.id/repositories"
 
14
  Service[models.Account, models.Account]
15
  }
16
 
17
+ func ValidatePassword(password string) models.Exception {
18
+ var (
19
+ hasMinLen = false
20
+ hasUpper = false
21
+ hasLower = false
22
+ hasNumber = false
23
+ hasSpecial = false
24
+ )
25
+
26
+ if len(password) >= 8 {
27
+ hasMinLen = true
28
+ }
29
+
30
+ for _, char := range password {
31
+ switch {
32
+ case unicode.IsUpper(char):
33
+ hasUpper = true
34
+ break
35
+ case unicode.IsLower(char):
36
+ hasLower = true
37
+ break
38
+ case unicode.IsDigit(char):
39
+ hasNumber = true
40
+ break
41
+ case unicode.IsPunct(char) || unicode.IsSymbol(char):
42
+ hasSpecial = true
43
+ break
44
+ }
45
+ }
46
+ approve := hasMinLen && hasUpper && hasLower && hasNumber && hasSpecial
47
+ if !approve {
48
+ return models.Exception{
49
+ BadRequest: true,
50
+ Message: "Password must contain at least 8 characters, including uppercase, lowercase, number, and special character!",
51
+ }
52
+ }
53
+ return models.Exception{}
54
+ }
55
+
56
  func (s *RegisterService) Create() {
57
+ validatePassword := ValidatePassword(s.Constructor.Password)
58
+ if validatePassword.BadRequest {
59
+ s.Exception = validatePassword
60
  return
61
  }
62
  hashed_password, err_hash := HashPassword(s.Constructor.Password)
space/space/space/space/space/controller/academy/academy_contents_controller.go ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package academy
2
+
3
+ import (
4
+ "api.qobiltu.id/controller"
5
+ "api.qobiltu.id/models"
6
+ "api.qobiltu.id/services"
7
+ "github.com/gin-gonic/gin"
8
+ )
9
+
10
+ func ContentList(c *gin.Context) {
11
+ academyContent := services.AcademyContentService{}
12
+ academyController := controller.Controller[any, models.AcademyMaterial, []models.AcademyContent]{
13
+ Service: &academyContent.Service,
14
+ }
15
+ academyController.Service.Constructor.Slug = c.Param("slug_material")
16
+ academyContent.Retrieve()
17
+ academyController.Response(c)
18
+
19
+ }
space/space/space/space/space/controller/academy/academy_controller.go CHANGED
@@ -9,7 +9,7 @@ import (
9
 
10
  func List(c *gin.Context) {
11
  academy := services.AcademyService{}
12
- academyController := controller.Controller[any, models.Academy, models.AllAcademyResponse]{
13
  Service: &academy.Service,
14
  }
15
  academyController.Service.Constructor.Slug = c.Param("slug")
 
9
 
10
  func List(c *gin.Context) {
11
  academy := services.AcademyService{}
12
+ academyController := controller.Controller[any, models.Academy, []models.Academy]{
13
  Service: &academy.Service,
14
  }
15
  academyController.Service.Constructor.Slug = c.Param("slug")
space/space/space/space/space/controller/academy/academy_materials_controller.go ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package academy
2
+
3
+ import (
4
+ "api.qobiltu.id/controller"
5
+ "api.qobiltu.id/models"
6
+ "api.qobiltu.id/services"
7
+ "github.com/gin-gonic/gin"
8
+ )
9
+
10
+ func MaterialsList(c *gin.Context) {
11
+ academyMaterial := services.AcademyMaterialService{}
12
+ academyController := controller.Controller[any, models.Academy, []models.AcademyMaterial]{
13
+ Service: &academyMaterial.Service,
14
+ }
15
+ academyController.Service.Constructor.Slug = c.Param("slug")
16
+ academyMaterial.Retrieve()
17
+ academyController.Response(c)
18
+
19
+ }
space/space/space/space/space/models/database_orm_model.go CHANGED
@@ -68,11 +68,13 @@ type ForgotPassword struct {
68
  }
69
 
70
  type Academy struct {
71
- ID uint `gorm:"primaryKey" json:"id"`
72
- UUID uuid.UUID `gorm:"type:uuid" json:"uuid"`
73
- Title string `json:"title"`
74
- Slug string `json:"slug"`
75
- Description string `json:"description"`
 
 
76
  }
77
 
78
  type AcademyMaterial struct {
@@ -81,6 +83,7 @@ type AcademyMaterial struct {
81
  AcademyID uint `json:"academy_id"`
82
  Title string `json:"title"`
83
  Slug string `json:"slug"`
 
84
  Description string `json:"description"`
85
  }
86
 
 
68
  }
69
 
70
  type Academy struct {
71
+ ID uint `gorm:"primaryKey" json:"id"`
72
+ UUID uuid.UUID `gorm:"type:uuid" json:"uuid"`
73
+ Title string `json:"title"`
74
+ Slug string `json:"slug"`
75
+ TotalMaterial int `json:"total_material"`
76
+ CompletedMaterial int `json:"completed_material"`
77
+ Description string `json:"description"`
78
  }
79
 
80
  type AcademyMaterial struct {
 
83
  AcademyID uint `json:"academy_id"`
84
  Title string `json:"title"`
85
  Slug string `json:"slug"`
86
+ IsCompleted bool `json:"is_completed"`
87
  Description string `json:"description"`
88
  }
89
 
space/space/space/space/space/repositories/academy_repository.go CHANGED
@@ -71,3 +71,14 @@ func CreateAcademyContent(academyContent models.AcademyContent) Repository[model
71
  Create(repo)
72
  return *repo
73
  }
 
 
 
 
 
 
 
 
 
 
 
 
71
  Create(repo)
72
  return *repo
73
  }
74
+
75
+ func GetAcademyMaterialBySlug(slug string) Repository[models.AcademyMaterial, models.AcademyMaterial] {
76
+ repo := Construct[models.AcademyMaterial, models.AcademyMaterial](
77
+ models.AcademyMaterial{Slug: slug},
78
+ )
79
+ repo.Transactions(
80
+ WhereGivenConstructor[models.AcademyMaterial, models.AcademyMaterial],
81
+ Find[models.AcademyMaterial, models.AcademyMaterial],
82
+ )
83
+ return *repo
84
+ }
space/space/space/space/space/router/academy_route.go CHANGED
@@ -9,7 +9,8 @@ import (
9
  func AcademyRoute(router *gin.Engine) {
10
  routerGroup := router.Group("/api/v1/academy")
11
  {
12
- routerGroup.GET("/list/:slug", middleware.AuthUser, AcademyController.List)
 
13
  routerGroup.GET("/list", middleware.AuthUser, AcademyController.List)
14
  routerGroup.POST("/create", middleware.AuthUser, AcademyController.Create)
15
  }
 
9
  func AcademyRoute(router *gin.Engine) {
10
  routerGroup := router.Group("/api/v1/academy")
11
  {
12
+ routerGroup.GET("/:slug", middleware.AuthUser, AcademyController.MaterialsList)
13
+ routerGroup.GET("/:slug/:slug_material", middleware.AuthUser, AcademyController.ContentList)
14
  routerGroup.GET("/list", middleware.AuthUser, AcademyController.List)
15
  routerGroup.POST("/create", middleware.AuthUser, AcademyController.Create)
16
  }
space/space/space/space/space/services/academy_service.go CHANGED
@@ -8,13 +8,21 @@ import (
8
  )
9
 
10
  type AcademyService struct {
11
- Service[models.Academy, models.AllAcademyResponse]
12
  }
13
 
14
  type CreateAcademyService struct {
15
  Service[models.AllAcademyResponse, models.AllAcademyResponse]
16
  }
17
 
 
 
 
 
 
 
 
 
18
  func castAcademyMaterials(academyId uint) []models.AcademyMaterialResponse {
19
  var ArrMaterials []models.AcademyMaterialResponse
20
  for _, academyMaterial := range repositories.GetAllAcademyMaterialsByAcademyID(academyId).Result {
@@ -35,27 +43,13 @@ func (s *AcademyService) Retrieve() {
35
  return
36
  }
37
 
38
- s.Result = models.AllAcademyResponse{
39
- Academies: []models.AcademyResponse{
40
- models.AcademyResponse{
41
- Academy: AcademyRepo.Result,
42
- Materials: castAcademyMaterials(s.Constructor.ID),
43
- },
44
- },
45
  }
46
  } else {
47
  AcademyRepo := repositories.GetAllAcademy()
48
  s.Error = AcademyRepo.RowsError
49
- var ArrAcademy []models.AcademyResponse
50
- for _, academy := range AcademyRepo.Result {
51
- ArrAcademy = append(ArrAcademy, models.AcademyResponse{
52
- Academy: academy,
53
- Materials: castAcademyMaterials(academy.ID),
54
- })
55
- }
56
- s.Result = models.AllAcademyResponse{
57
- Academies: ArrAcademy,
58
- }
59
  }
60
 
61
  }
@@ -102,3 +96,27 @@ func (s *CreateAcademyService) Create() {
102
  s.Result = models.AllAcademyResponse{Academies: ArrAcademy}
103
 
104
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  )
9
 
10
  type AcademyService struct {
11
+ Service[models.Academy, []models.Academy]
12
  }
13
 
14
  type CreateAcademyService struct {
15
  Service[models.AllAcademyResponse, models.AllAcademyResponse]
16
  }
17
 
18
+ type AcademyMaterialService struct {
19
+ Service[models.Academy, []models.AcademyMaterial]
20
+ }
21
+
22
+ type AcademyContentService struct {
23
+ Service[models.AcademyMaterial, []models.AcademyContent]
24
+ }
25
+
26
  func castAcademyMaterials(academyId uint) []models.AcademyMaterialResponse {
27
  var ArrMaterials []models.AcademyMaterialResponse
28
  for _, academyMaterial := range repositories.GetAllAcademyMaterialsByAcademyID(academyId).Result {
 
43
  return
44
  }
45
 
46
+ s.Result = []models.Academy{
47
+ AcademyRepo.Result,
 
 
 
 
 
48
  }
49
  } else {
50
  AcademyRepo := repositories.GetAllAcademy()
51
  s.Error = AcademyRepo.RowsError
52
+ s.Result = AcademyRepo.Result
 
 
 
 
 
 
 
 
 
53
  }
54
 
55
  }
 
96
  s.Result = models.AllAcademyResponse{Academies: ArrAcademy}
97
 
98
  }
99
+
100
+ func (s *AcademyMaterialService) Retrieve() {
101
+ academyId := repositories.GetAcademyDataBySlug(s.Constructor.Slug).Result.ID
102
+ academyMaterialRepo := repositories.GetAllAcademyMaterialsByAcademyID(academyId)
103
+ s.Error = academyMaterialRepo.RowsError
104
+ if academyMaterialRepo.NoRecord {
105
+ s.Exception.DataNotFound = true
106
+ s.Exception.Message = "There is no Academy Material with given ID"
107
+ return
108
+ }
109
+ s.Result = academyMaterialRepo.Result
110
+ }
111
+
112
+ func (s *AcademyContentService) Retrieve() {
113
+ academyMaterialId := repositories.GetAcademyMaterialBySlug(s.Constructor.Slug).Result.ID
114
+ academyContentRepo := repositories.GetAllAcademyContentsByMaterialID(academyMaterialId)
115
+ s.Error = academyContentRepo.RowsError
116
+ if academyContentRepo.NoRecord {
117
+ s.Exception.DataNotFound = true
118
+ s.Exception.Message = "There is no Academy Contents with given Material ID"
119
+ return
120
+ }
121
+ s.Result = academyContentRepo.Result
122
+ }
space/space/space/space/space/services/jwt_service.go CHANGED
@@ -20,7 +20,7 @@ func GenerateToken(user *models.Account) (string, error) {
20
  RegisteredClaims: jwt.RegisteredClaims{
21
  ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24)), // Token berlaku 24 jam
22
  IssuedAt: jwt.NewNumericDate(time.Now()),
23
- Issuer: "qobiltu.id",
24
  },
25
  }
26
 
 
20
  RegisteredClaims: jwt.RegisteredClaims{
21
  ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24)), // Token berlaku 24 jam
22
  IssuedAt: jwt.NewNumericDate(time.Now()),
23
+ Issuer: "apqobiltu.id",
24
  },
25
  }
26
 
space/space/space/space/space/services/service.go CHANGED
@@ -16,6 +16,9 @@ type (
16
  Authenticate()
17
  Authorize()
18
  }
 
 
 
19
  Service[TConstructor any, TResult any] struct {
20
  Constructor TConstructor
21
  Result TResult
 
16
  Authenticate()
17
  Authorize()
18
  }
19
+ IService interface {
20
+ Implements()
21
+ }
22
  Service[TConstructor any, TResult any] struct {
23
  Constructor TConstructor
24
  Result TResult
space/space/space/space/space/space/services/email_verification_service.go CHANGED
@@ -75,18 +75,7 @@ func (s *EmailVerificationService) Create() {
75
  return
76
  }
77
 
78
- // 2. Start TLS
79
- tlsconfig := &tls.Config{
80
- ServerName: smtpHost,
81
- }
82
-
83
- if err = c.StartTLS(tlsconfig); err != nil {
84
- s.Error = err
85
- log.Printf("Error start TLS: %v", err)
86
- return
87
- }
88
-
89
- // 3. Auth
90
  auth := smtp.PlainAuth("", from, password, smtpHost)
91
  if err = c.Auth(auth); err != nil {
92
  s.Error = err
@@ -94,7 +83,7 @@ func (s *EmailVerificationService) Create() {
94
  return
95
  }
96
 
97
- // 4. Set From and To
98
  if err = c.Mail(from); err != nil {
99
  s.Error = err
100
  log.Printf("Error set mail from to: %v", err)
@@ -109,7 +98,7 @@ func (s *EmailVerificationService) Create() {
109
  }
110
  }
111
 
112
- // 5. Send message
113
  wc, err := c.Data()
114
  if err != nil {
115
  s.Error = err
 
75
  return
76
  }
77
 
78
+ // 2. Auth
 
 
 
 
 
 
 
 
 
 
 
79
  auth := smtp.PlainAuth("", from, password, smtpHost)
80
  if err = c.Auth(auth); err != nil {
81
  s.Error = err
 
83
  return
84
  }
85
 
86
+ // 3. Set From and To
87
  if err = c.Mail(from); err != nil {
88
  s.Error = err
89
  log.Printf("Error set mail from to: %v", err)
 
98
  }
99
  }
100
 
101
+ // 4. Send message
102
  wc, err := c.Data()
103
  if err != nil {
104
  s.Error = err
space/space/space/space/space/space/space/space/config/config.go CHANGED
@@ -7,6 +7,7 @@ import (
7
  "github.com/joho/godotenv"
8
  )
9
 
 
10
  var TCP_ADDRESS string
11
  var LOG_PATH string
12
 
@@ -21,6 +22,7 @@ var SMTP_PORT string
21
 
22
  func init() {
23
  godotenv.Load()
 
24
  HOST_ADDRESS = os.Getenv("HOST_ADDRESS")
25
  HOST_PORT = os.Getenv("HOST_PORT")
26
  TCP_ADDRESS = HOST_ADDRESS + ":" + HOST_PORT
 
7
  "github.com/joho/godotenv"
8
  )
9
 
10
+ var ENV string
11
  var TCP_ADDRESS string
12
  var LOG_PATH string
13
 
 
22
 
23
  func init() {
24
  godotenv.Load()
25
+ ENV = os.Getenv("ENV")
26
  HOST_ADDRESS = os.Getenv("HOST_ADDRESS")
27
  HOST_PORT = os.Getenv("HOST_PORT")
28
  TCP_ADDRESS = HOST_ADDRESS + ":" + HOST_PORT
space/space/space/space/space/space/space/space/services/email_verification_service.go CHANGED
@@ -1,6 +1,7 @@
1
  package services
2
 
3
  import (
 
4
  "fmt"
5
  "log"
6
  "math/rand/v2"
@@ -39,27 +40,118 @@ func (s *EmailVerificationService) Create() {
39
 
40
  // ⬇ Kirim token ke email user menggunakan SMTP
41
  go func(toEmail string, token uint) {
 
42
  from := config.SMTP_SENDER_EMAIL
43
  password := config.SMTP_SENDER_PASSWORD
44
  smtpHost := config.SMTP_HOST
45
  smtpPort := config.SMTP_PORT
 
46
 
47
- auth := smtp.PlainAuth("", from, password, smtpHost)
48
-
49
- subject := "Email Verification Token"
50
  body := fmt.Sprintf("Your verification token is: %06d\nPlease use it before it expires.", token)
51
 
52
- msg := []byte("To: " + toEmail + "\r\n" +
53
- "Subject: " + subject + "\r\n" +
54
- "\r\n" +
55
- body + "\r\n")
 
 
 
 
 
 
56
 
57
- err := smtp.SendMail(smtpHost+":"+smtpPort, auth, from, []string{toEmail}, msg)
58
  if err != nil {
59
  s.Error = err
60
  log.Printf("Error sending verification email: %v", err)
61
  return
62
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  }(accountRepo.Result.Email, token)
64
  // s.Result.Token = 0
65
  }
 
1
  package services
2
 
3
  import (
4
+ "crypto/tls"
5
  "fmt"
6
  "log"
7
  "math/rand/v2"
 
40
 
41
  // ⬇ Kirim token ke email user menggunakan SMTP
42
  go func(toEmail string, token uint) {
43
+ env := config.ENV
44
  from := config.SMTP_SENDER_EMAIL
45
  password := config.SMTP_SENDER_PASSWORD
46
  smtpHost := config.SMTP_HOST
47
  smtpPort := config.SMTP_PORT
48
+ to := []string{toEmail}
49
 
50
+ subject := "Verification token"
 
 
51
  body := fmt.Sprintf("Your verification token is: %06d\nPlease use it before it expires.", token)
52
 
53
+ msg := []byte(fmt.Sprintf("From: %s\r\n", from) +
54
+ fmt.Sprintf("To: %s\r\n", toEmail) +
55
+ fmt.Sprintf("Subject: %s\r\n", subject) +
56
+ "\r\n" + body)
57
+
58
+ // 1. Connect to the server
59
+ conn, err := tls.Dial("tcp", fmt.Sprintf("[%s]:%s", smtpHost, smtpPort), &tls.Config{
60
+ InsecureSkipVerify: env != "production", // ⚠️ set false di production
61
+ ServerName: smtpHost,
62
+ })
63
 
64
+ // conn, err := net.Dial("tcp", fmt.Sprintf("[%s]:%s", smtpHost, smtpPort))
65
  if err != nil {
66
  s.Error = err
67
  log.Printf("Error sending verification email: %v", err)
68
  return
69
  }
70
+
71
+ c, err := smtp.NewClient(conn, smtpHost)
72
+ if err != nil {
73
+ s.Error = err
74
+ log.Printf("Error create new client mail: %v", err)
75
+ return
76
+ }
77
+
78
+ // 2. Start TLS
79
+ tlsconfig := &tls.Config{
80
+ ServerName: smtpHost,
81
+ }
82
+
83
+ if err = c.StartTLS(tlsconfig); err != nil {
84
+ s.Error = err
85
+ log.Printf("Error start TLS: %v", err)
86
+ return
87
+ }
88
+
89
+ // 3. Auth
90
+ auth := smtp.PlainAuth("", from, password, smtpHost)
91
+ if err = c.Auth(auth); err != nil {
92
+ s.Error = err
93
+ log.Printf("Error auth mail: %v", err)
94
+ return
95
+ }
96
+
97
+ // 4. Set From and To
98
+ if err = c.Mail(from); err != nil {
99
+ s.Error = err
100
+ log.Printf("Error set mail from to: %v", err)
101
+ return
102
+ }
103
+
104
+ for _, addr := range to {
105
+ if err = c.Rcpt(addr); err != nil {
106
+ s.Error = err
107
+ log.Printf("Error receipt addr: %v", err)
108
+ return
109
+ }
110
+ }
111
+
112
+ // 5. Send message
113
+ wc, err := c.Data()
114
+ if err != nil {
115
+ s.Error = err
116
+ log.Printf("Error data Send Message: %v", err)
117
+ return
118
+ }
119
+
120
+ _, err = wc.Write(msg)
121
+ if err != nil {
122
+ s.Error = err
123
+ log.Printf("Error write Send Message: %v", err)
124
+ return
125
+ }
126
+
127
+ err = wc.Close()
128
+ if err != nil {
129
+ s.Error = err
130
+ log.Printf("Error close Send Message: %v", err)
131
+ return
132
+ }
133
+
134
+ c.Quit()
135
+ fmt.Println("Email sent successfully!")
136
+
137
+ // auth := smtp.PlainAuth("", from, password, smtpHost)
138
+
139
+ // subject := "Email Verification Token"
140
+ // body := fmt.Sprintf("Your verification token is: %06d\nPlease use it before it expires.", token)
141
+
142
+ // msg := []byte("To: " + toEmail + "\r\n" +
143
+ // "Subject: " + subject + "\r\n" +
144
+ // "\r\n" +
145
+ // body + "\r\n")
146
+
147
+ // err := smtp.SendMail(smtpHost+":"+smtpPort, auth, from, []string{toEmail}, msg)
148
+ // if err != nil {
149
+ // s.Error = err
150
+ // log.Printf("Error sending verification email: %v", err)
151
+ // return
152
+ // } else {
153
+ // log.Printf("Successfully sending verification email: %v", err)
154
+ // }
155
  }(accountRepo.Result.Email, token)
156
  // s.Result.Token = 0
157
  }
space/space/space/space/space/space/space/space/space/services/external_authentication_service.go CHANGED
@@ -24,6 +24,16 @@ func (s *GoogleAuthService) Authenticate(isAgree bool) {
24
  return
25
  }
26
  email := payload.Claims["email"]
 
 
 
 
 
 
 
 
 
 
27
  if GoogleAuth.NoRecord {
28
  if !isAgree {
29
  s.Exception.BadRequest = true
@@ -32,12 +42,6 @@ func (s *GoogleAuthService) Authenticate(isAgree bool) {
32
  }
33
  s.Constructor.UUID = uuid.NewV4()
34
  s.Constructor.OauthProvider = "Google"
35
- checkRegisteredEmail := repositories.GetAccountbyEmail(email.(string))
36
- if !checkRegisteredEmail.NoRecord {
37
- s.Exception.DataDuplicate = true
38
- s.Exception.Message = "Account with email" + email.(string) + "already registered!"
39
- return
40
- }
41
 
42
  createAccount := repositories.CreateAccount(models.Account{
43
  UUID: uuid.NewV4(),
 
24
  return
25
  }
26
  email := payload.Claims["email"]
27
+ checkRegisteredEmail := repositories.GetAccountbyEmail(email.(string))
28
+ if !checkRegisteredEmail.NoRecord {
29
+ token, _ := GenerateToken(&checkRegisteredEmail.Result)
30
+ checkRegisteredEmail.Result.Password = "SECRET"
31
+ s.Result = models.AuthenticatedUser{
32
+ Account: checkRegisteredEmail.Result,
33
+ Token: token,
34
+ }
35
+ return
36
+ }
37
  if GoogleAuth.NoRecord {
38
  if !isAgree {
39
  s.Exception.BadRequest = true
 
42
  }
43
  s.Constructor.UUID = uuid.NewV4()
44
  s.Constructor.OauthProvider = "Google"
 
 
 
 
 
 
45
 
46
  createAccount := repositories.CreateAccount(models.Account{
47
  UUID: uuid.NewV4(),
space/space/space/space/space/space/space/space/space/space/services/email_verification_service.go CHANGED
@@ -29,10 +29,7 @@ func (s *EmailVerificationService) Create() {
29
  remainingTime := time.Duration(config.EMAIL_VERIFICATION_DURATION) * time.Hour
30
  dueTime := CalculateDueTime(remainingTime)
31
 
32
- token := uint(rand.IntN(1000000))
33
- for token < 1000000 {
34
- token = uint(rand.IntN(1000000))
35
- }
36
  s.Constructor.UUID = uuid.NewV4()
37
 
38
  repo := repositories.CreateEmailVerification(s.Constructor.UUID, s.Constructor.AccountID, dueTime, token)
 
29
  remainingTime := time.Duration(config.EMAIL_VERIFICATION_DURATION) * time.Hour
30
  dueTime := CalculateDueTime(remainingTime)
31
 
32
+ token := uint(rand.IntN(999999-100000) + 100000)
 
 
 
33
  s.Constructor.UUID = uuid.NewV4()
34
 
35
  repo := repositories.CreateEmailVerification(s.Constructor.UUID, s.Constructor.AccountID, dueTime, token)
space/space/space/space/space/space/space/space/space/space/services/forgot_password_service.go CHANGED
@@ -34,10 +34,7 @@ func (s *ForgotPasswordService) Create(email string) {
34
  remainingTime := time.Duration(config.EMAIL_VERIFICATION_DURATION) * time.Hour
35
  dueTime := CalculateDueTime(remainingTime)
36
 
37
- token := uint(rand.IntN(1000000))
38
- for token < 1000000 {
39
- token = uint(rand.IntN(1000000))
40
- }
41
  s.Constructor.UUID = uuid.NewV4()
42
  s.Constructor.ExpiredAt = dueTime
43
  s.Constructor.AccountID = accountRepo.Result.Id
 
34
  remainingTime := time.Duration(config.EMAIL_VERIFICATION_DURATION) * time.Hour
35
  dueTime := CalculateDueTime(remainingTime)
36
 
37
+ token := uint(rand.IntN(999999-100000) + 100000)
 
 
 
38
  s.Constructor.UUID = uuid.NewV4()
39
  s.Constructor.ExpiredAt = dueTime
40
  s.Constructor.AccountID = accountRepo.Result.Id
space/space/space/space/space/space/space/space/space/space/space/services/external_authentication_service.go CHANGED
@@ -32,17 +32,34 @@ func (s *GoogleAuthService) Authenticate(isAgree bool) {
32
  }
33
  s.Constructor.UUID = uuid.NewV4()
34
  s.Constructor.OauthProvider = "Google"
 
 
 
 
 
 
 
35
  createAccount := repositories.CreateAccount(models.Account{
36
  UUID: uuid.NewV4(),
37
  Email: email.(string),
38
  IsEmailVerified: true,
39
  })
 
40
  s.Constructor.AccountID = createAccount.Result.Id
41
  createGoogleAuth := repositories.CreateExternalAuth(s.Constructor)
42
- GoogleAuth.Result.AccountID = createGoogleAuth.Result.ID
 
 
 
 
 
 
 
 
43
  s.Error = createGoogleAuth.RowsError
44
  s.Error = errors.Join(s.Error, createAccount.RowsError)
45
  }
 
46
  accountData := repositories.GetAccountById(GoogleAuth.Result.AccountID)
47
  token, err_tok := GenerateToken(&accountData.Result)
48
 
 
32
  }
33
  s.Constructor.UUID = uuid.NewV4()
34
  s.Constructor.OauthProvider = "Google"
35
+ checkRegisteredEmail := repositories.GetAccountbyEmail(email.(string))
36
+ if !checkRegisteredEmail.NoRecord {
37
+ s.Exception.DataDuplicate = true
38
+ s.Exception.Message = "Account with email" + email.(string) + "already registered!"
39
+ return
40
+ }
41
+
42
  createAccount := repositories.CreateAccount(models.Account{
43
  UUID: uuid.NewV4(),
44
  Email: email.(string),
45
  IsEmailVerified: true,
46
  })
47
+
48
  s.Constructor.AccountID = createAccount.Result.Id
49
  createGoogleAuth := repositories.CreateExternalAuth(s.Constructor)
50
+
51
+ GoogleAuth.Result.AccountID = createGoogleAuth.Result.AccountID
52
+ userProfile := UserProfileService{}
53
+ userProfile.Constructor.AccountID = GoogleAuth.Result.AccountID
54
+ userProfile.Create()
55
+ if userProfile.Error != nil {
56
+ s.Error = userProfile.Error
57
+ return
58
+ }
59
  s.Error = createGoogleAuth.RowsError
60
  s.Error = errors.Join(s.Error, createAccount.RowsError)
61
  }
62
+
63
  accountData := repositories.GetAccountById(GoogleAuth.Result.AccountID)
64
  token, err_tok := GenerateToken(&accountData.Result)
65
 
space/space/space/space/space/space/space/space/space/space/space/space/controller/academy/academy_controller.go ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package academy
2
+
3
+ import (
4
+ "api.qobiltu.id/controller"
5
+ "api.qobiltu.id/models"
6
+ "api.qobiltu.id/services"
7
+ "github.com/gin-gonic/gin"
8
+ )
9
+
10
+ func List(c *gin.Context) {
11
+ academy := services.AcademyService{}
12
+ academyController := controller.Controller[any, models.Academy, models.AllAcademyResponse]{
13
+ Service: &academy.Service,
14
+ }
15
+ academyController.Service.Constructor.Slug = c.Param("slug")
16
+ academy.Retrieve()
17
+ academyController.Response(c)
18
+
19
+ }
20
+
21
+ func Create(c *gin.Context) {
22
+ createAcademy := services.CreateAcademyService{}
23
+ academyController := controller.Controller[models.AllAcademyResponse, models.AllAcademyResponse, models.AllAcademyResponse]{
24
+ Service: &createAcademy.Service,
25
+ }
26
+ academyController.RequestJSON(c, func() {
27
+ academyController.Service.Constructor.Academies = academyController.Request.Academies
28
+ createAcademy.Create()
29
+ })
30
+
31
+ }
space/space/space/space/space/space/space/space/space/space/space/space/models/database_orm_model.go CHANGED
@@ -30,7 +30,7 @@ type AccountDetails struct {
30
  LastEducation *string `json:"last_education"`
31
  MaritalStatus *string `json:"marital_status"`
32
  Avatar *string `json:"avatar"`
33
- PhoneNumber *uint `json:"phone_number"`
34
  }
35
 
36
  type EmailVerification struct {
@@ -85,12 +85,12 @@ type AcademyMaterial struct {
85
  }
86
 
87
  type AcademyContent struct {
88
- ID uint `gorm:"primaryKey" json:"id"`
89
- UUID uint `json:"uuid"`
90
- Title string `json:"title"`
91
- Order uint `json:"order"`
92
- AcademyMaterialID uint `json:"academy_material_id"`
93
- Description string `json:"description"`
94
  }
95
  type OptionCategory struct {
96
  ID uint `gorm:"primaryKey" json:"id"`
 
30
  LastEducation *string `json:"last_education"`
31
  MaritalStatus *string `json:"marital_status"`
32
  Avatar *string `json:"avatar"`
33
+ PhoneNumber *string `json:"phone_number"`
34
  }
35
 
36
  type EmailVerification struct {
 
85
  }
86
 
87
  type AcademyContent struct {
88
+ ID uint `gorm:"primaryKey" json:"id"`
89
+ UUID uuid.UUID `json:"uuid"`
90
+ Title string `json:"title"`
91
+ Order uint `json:"order"`
92
+ AcademyMaterialID uint `json:"academy_material_id"`
93
+ Description string `json:"description"`
94
  }
95
  type OptionCategory struct {
96
  ID uint `gorm:"primaryKey" json:"id"`