lifedebugger commited on
Commit
2bf583e
·
1 Parent(s): a3e77a9

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. services/health_check_service.go +1 -2
  2. space/.gitignore +1 -0
  3. space/controller/health_check/health_check_controller.go +12 -8
  4. space/main.go +2 -2
  5. space/models/database_orm_model.go +207 -207
  6. space/models/exception_model.go +19 -13
  7. space/models/health_check_model.go +1 -3
  8. space/repositories/health_check_repository.go +12 -13
  9. space/response/api_response_v2.go +79 -0
  10. space/response/gorm.go +62 -0
  11. space/router/server.go +2 -2
  12. space/services/health_check_service.go +32 -0
  13. space/services/user_profile_service.go +115 -115
  14. space/space/Dockerfile +6 -0
  15. space/space/space/.github/workflows/main.yml +1 -0
  16. space/space/space/apperror/apperror.go +168 -0
  17. space/space/space/config/database_connection_config.go +80 -79
  18. space/space/space/config/tx.go +34 -0
  19. space/space/space/controller/health_check/health_check_controller.go +31 -0
  20. space/space/space/models/exception_model.go +17 -15
  21. space/space/space/models/health_check_model.go +10 -0
  22. space/space/space/repositories/health_check_repository.go +41 -0
  23. space/space/space/response/paging.go +58 -0
  24. space/space/space/response/response.go +152 -0
  25. space/space/space/router/health_check_route.go +5 -0
  26. space/space/space/router/router.go +12 -19
  27. space/space/space/router/server.go +32 -0
  28. space/space/space/services/health_check_service/health_check_service.go +15 -0
  29. space/space/space/services/health_check_service/health_check_service_check.go +21 -0
  30. space/space/space/services/register_service.go +66 -66
  31. space/space/space/space/.gitignore +2 -1
  32. space/space/space/space/Makefile +7 -0
  33. space/space/space/space/assets/efs.go +13 -0
  34. space/space/space/space/assets/emails/email-confirmation.tmpl +55 -0
  35. space/space/space/space/assets/emails/email-forgot-password.tmpl +56 -0
  36. space/space/space/space/config/config.go +63 -36
  37. space/space/space/space/controller/api_response.go +53 -0
  38. space/space/space/space/controller/controller.go +65 -65
  39. space/space/space/space/controller/email/email_create_verification_controller.go +21 -21
  40. space/space/space/space/docker-compose.dev.yml +18 -0
  41. space/space/space/space/docker-compose.yml +23 -0
  42. space/space/space/space/go.mod +9 -0
  43. space/space/space/space/go.sum +22 -0
  44. space/space/space/space/mail/sender.go +8 -0
  45. space/space/space/space/mail/smtp.go +71 -0
  46. space/space/space/space/main.go +62 -14
  47. space/space/space/space/middleware/authentication_middleware.go +37 -37
  48. space/space/space/space/router/router.go +27 -24
  49. space/space/space/space/services/email_verification_service.go +101 -181
  50. space/space/space/space/services/forgot_password_service.go +114 -113
services/health_check_service.go CHANGED
@@ -5,7 +5,6 @@ import (
5
  "api.qobiltu.id/repositories"
6
  "api.qobiltu.id/response"
7
  "context"
8
- "gorm.io/gorm"
9
  )
10
 
11
  type HealthCheckService interface {
@@ -25,7 +24,7 @@ func NewHealthCheckService(healthCheckRepository repositories.HealthCheckReposit
25
  func (s *healthCheckService) Check(ctx context.Context, req *models.HealthCheckRequest) (*models.HealthCheckResponse, error) {
26
  res, err := s.healthCheckRepository.Check(ctx, req)
27
  if err != nil {
28
- return nil, response.HandleGormError(gorm.ErrDuplicatedKey, "Internal Server Error")
29
  }
30
 
31
  return res, nil
 
5
  "api.qobiltu.id/repositories"
6
  "api.qobiltu.id/response"
7
  "context"
 
8
  )
9
 
10
  type HealthCheckService interface {
 
24
  func (s *healthCheckService) Check(ctx context.Context, req *models.HealthCheckRequest) (*models.HealthCheckResponse, error) {
25
  res, err := s.healthCheckRepository.Check(ctx, req)
26
  if err != nil {
27
+ return nil, response.HandleGormError(err, "Internal Server Error")
28
  }
29
 
30
  return res, nil
space/.gitignore CHANGED
@@ -6,3 +6,4 @@ README.md
6
  .error
7
  logs/
8
  .idea
 
 
6
  .error
7
  logs/
8
  .idea
9
+ my-notes
space/controller/health_check/health_check_controller.go CHANGED
@@ -3,29 +3,33 @@ package health_check_controller
3
  import (
4
  "api.qobiltu.id/models"
5
  "api.qobiltu.id/response"
6
- "api.qobiltu.id/services/health_check_service"
7
  "github.com/gin-gonic/gin"
8
  "net/http"
9
  )
10
 
11
- type HealthCheckController struct {
12
- healthCheckService *health_check_service.HealthCheckService
13
  }
14
 
15
- func NewHealthCheckController(healthCheckService *health_check_service.HealthCheckService) *HealthCheckController {
16
- return &HealthCheckController{
 
 
 
 
17
  healthCheckService: healthCheckService,
18
  }
19
  }
20
 
21
- func (h *HealthCheckController) Check(ctx *gin.Context) {
22
  req := models.HealthCheckRequest{}
23
 
24
- res, err := h.healthCheckService.Check(ctx, &req)
25
  if err != nil {
26
  response.HandleError(ctx, err)
27
  return
28
  }
29
 
30
- response.HandleSuccess(ctx, "Service is running", http.StatusOK, res, nil)
31
  }
 
3
  import (
4
  "api.qobiltu.id/models"
5
  "api.qobiltu.id/response"
6
+ "api.qobiltu.id/services"
7
  "github.com/gin-gonic/gin"
8
  "net/http"
9
  )
10
 
11
+ type HealthCheckController interface {
12
+ Check(ctx *gin.Context)
13
  }
14
 
15
+ type healthCheckController struct {
16
+ healthCheckService services.HealthCheckService
17
+ }
18
+
19
+ func NewHealthCheckController(healthCheckService services.HealthCheckService) HealthCheckController {
20
+ return &healthCheckController{
21
  healthCheckService: healthCheckService,
22
  }
23
  }
24
 
25
+ func (c *healthCheckController) Check(ctx *gin.Context) {
26
  req := models.HealthCheckRequest{}
27
 
28
+ res, err := c.healthCheckService.Check(ctx, &req)
29
  if err != nil {
30
  response.HandleError(ctx, err)
31
  return
32
  }
33
 
34
+ response.HandleSuccess(ctx, http.StatusOK, "Service OK", res, nil)
35
  }
space/main.go CHANGED
@@ -6,7 +6,7 @@ import (
6
  "api.qobiltu.id/mail"
7
  "api.qobiltu.id/repositories"
8
  "api.qobiltu.id/router"
9
- "api.qobiltu.id/services/health_check_service"
10
  "api.qobiltu.id/utils"
11
  "api.qobiltu.id/worker"
12
  "github.com/hibiken/asynq"
@@ -43,7 +43,7 @@ func main() {
43
 
44
  // setup repo, service, and controller
45
  healthCheckRepository := repositories.NewHealthCheckRepository(config.DB)
46
- healthCheckService := health_check_service.NewHealthCheckService(healthCheckRepository)
47
  healthCheckController := health_check_controller.NewHealthCheckController(healthCheckService)
48
 
49
  // start task processor
 
6
  "api.qobiltu.id/mail"
7
  "api.qobiltu.id/repositories"
8
  "api.qobiltu.id/router"
9
+ "api.qobiltu.id/services"
10
  "api.qobiltu.id/utils"
11
  "api.qobiltu.id/worker"
12
  "github.com/hibiken/asynq"
 
43
 
44
  // setup repo, service, and controller
45
  healthCheckRepository := repositories.NewHealthCheckRepository(config.DB)
46
+ healthCheckService := services.NewHealthCheckService(healthCheckRepository)
47
  healthCheckController := health_check_controller.NewHealthCheckController(healthCheckService)
48
 
49
  // start task processor
space/models/database_orm_model.go CHANGED
@@ -1,207 +1,207 @@
1
- package models
2
-
3
- import (
4
- "time"
5
-
6
- uuid "github.com/satori/go.uuid"
7
- )
8
-
9
- type Account struct {
10
- Id uint `gorm:"primaryKey" json:"id"`
11
- UUID uuid.UUID `gorm:"type:uuid" json:"uuid" `
12
- Email string `gorm:"uniqueIndex" json:"email"`
13
- Password string `json:"password"`
14
- IsEmailVerified bool `json:"is_email_verified"`
15
- IsDetailCompleted bool `json:"is_detail_completed"`
16
- CreatedAt time.Time `json:"created_at"`
17
- DeletedAt *time.Time `json:"deleted_at" gorm:"default:null"`
18
- }
19
-
20
- type AccountDetails struct {
21
- ID uint `gorm:"primaryKey" json:"id"`
22
- AccountID uint `json:"account_id"`
23
- InitialName string `json:"initial_name"`
24
- FullName *string `json:"full_name"`
25
- DateOfBirth *time.Time `json:"date_of_birth"`
26
- PlaceOfBirth *string `json:"place_of_birth"`
27
- Domicile *string `json:"domicile"`
28
- LastJob *string `json:"last_job"`
29
- Gender *bool `json:"gender"`
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 {
37
- ID uint `gorm:"primaryKey" json:"id"`
38
- UUID uuid.UUID `gorm:"type:uuid" json:"uuid" `
39
- Token uint `json:"token"`
40
- AccountID uint `json:"account_id"`
41
- IsExpired bool `json:"is_expired"`
42
- CreatedAt time.Time `json:"created_at"`
43
- ExpiredAt time.Time `json:"expired_at"`
44
- }
45
-
46
- type ExternalAuth struct {
47
- ID uint `gorm:"primaryKey" json:"id"`
48
- UUID uuid.UUID `gorm:"type:uuid" json:"uuid" `
49
- OauthID string `json:"oauth_id"`
50
- AccountID uint `json:"account_id"`
51
- OauthProvider string `json:"oauth_provider"`
52
- }
53
-
54
- type FCM struct {
55
- ID uint `gorm:"primaryKey" json:"id"`
56
- AccountID uint `json:"account_id"`
57
- FCMToken string `json:"fcm_token"`
58
- }
59
-
60
- type ForgotPassword struct {
61
- ID uint `gorm:"primaryKey" json:"id"`
62
- UUID uuid.UUID `gorm:"type:uuid" json:"uuid" `
63
- Token uint `json:"token"`
64
- AccountID uint `json:"account_id"`
65
- IsExpired bool `json:"is_expired"`
66
- CreatedAt time.Time `json:"created_at"`
67
- ExpiredAt time.Time `json:"expired_at"`
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" 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
-
82
- type AcademyMaterial struct {
83
- ID uint `gorm:"primaryKey" json:"id"`
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
- }
91
-
92
- type AcademyContent struct {
93
- ID uint `gorm:"primaryKey" json:"id"`
94
- UUID uuid.UUID `json:"uuid"`
95
- Title string `json:"title"`
96
- Order uint `json:"order"`
97
- AcademyMaterialID uint `json:"academy_material_id"`
98
- Description string `json:"description"`
99
- }
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 {
107
- ID uint `gorm:"primaryKey" json:"id"`
108
- OptionCategoryID uint `json:"option_category_id"`
109
- OptionValue string `json:"option_value"`
110
- }
111
- type AcademyMaterialProgress struct {
112
- ID uint `gorm:"primaryKey" json:"id"`
113
- UUID uuid.UUID `gorm:"type:uuid" json:"uuid"`
114
- AccountID uint `json:"account_id"`
115
- AcademyMaterialID uint `json:"academy_material_id"`
116
- Progress uint `json:"progress"`
117
- }
118
-
119
- type AcademyContentProgress struct {
120
- ID uint `gorm:"primaryKey" json:"id"`
121
- UUID uuid.UUID `gorm:"type:uuid" json:"uuid"`
122
- AccountID uint `json:"account_id"`
123
- AcademyID uint `json:"academy_id"`
124
- }
125
-
126
- type RegionProvince struct {
127
- ID uint `json:"id"`
128
- Name string `json:"name"`
129
- Code string `json:"code"`
130
- }
131
-
132
- type RegionCity struct {
133
- ID uint `json:"id"`
134
- Type string `json:"type"`
135
- Name string `json:"name"`
136
- Code string `json:"code"`
137
- FullCode string `json:"full_code"`
138
- ProvinceID uint `json:"province_id"`
139
- }
140
- type Answer struct {
141
- ID uint `gorm:"primaryKey" json:"id"`
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" json:"id"`
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" json:"id"`
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" json:"id"`
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
- type QuizResult struct {
181
- QuizAttemptID uint `gorm:"column:quiz_attempt_id" json:"quiz_attempt_id"`
182
- TotalQuestions int `gorm:"column:total_questions" json:"total_questions"`
183
- CorrectAnswers int `gorm:"column:correct_answers" json:"correct_answers"`
184
- AverageScore float64 `gorm:"column:average_score" json:"average_score"`
185
- }
186
-
187
- // Gorm table name settings
188
- func (Account) TableName() string { return "account" }
189
- func (AccountDetails) TableName() string { return "account_details" }
190
- func (EmailVerification) TableName() string { return "email_verifications" }
191
- func (ExternalAuth) TableName() string { return "extern_auth" }
192
- func (FCM) TableName() string { return "fcm" }
193
- func (ForgotPassword) TableName() string { return "forgot_password" }
194
- func (Academy) TableName() string { return "academy" }
195
- func (AcademyMaterial) TableName() string { return "academy_materials" }
196
- func (AcademyContent) TableName() string { return "academy_contents" }
197
- func (AcademyMaterialProgress) TableName() string { return "academy_materials_progress" }
198
- func (AcademyContentProgress) TableName() string { return "academy_contents_progress" }
199
- func (RegionProvince) TableName() string { return "region_provinces" }
200
- func (RegionCity) TableName() string { return "region_cities" }
201
- func (Answer) TableName() string { return "answers" }
202
- func (Question) TableName() string { return "questions" }
203
- func (Quiz) TableName() string { return "quizzes" }
204
- func (QuizAttempt) TableName() string { return "quiz_attempts" }
205
- func (UserAnswer) TableName() string { return "user_answers" }
206
- func (OptionCategory) TableName() string { return "option_categories" }
207
- func (OptionValues) TableName() string { return "option_values" }
 
1
+ package models
2
+
3
+ import (
4
+ "time"
5
+
6
+ uuid "github.com/satori/go.uuid"
7
+ )
8
+
9
+ type Account struct {
10
+ Id uint `gorm:"primaryKey" json:"id"`
11
+ UUID uuid.UUID `gorm:"type:uuid" json:"uuid" `
12
+ Email string `gorm:"uniqueIndex" json:"email"`
13
+ Password string `json:"password"`
14
+ IsEmailVerified bool `json:"is_email_verified"`
15
+ IsDetailCompleted bool `json:"is_detail_completed"`
16
+ CreatedAt time.Time `json:"created_at"`
17
+ DeletedAt *time.Time `json:"deleted_at" gorm:"default:null"`
18
+ }
19
+
20
+ type AccountDetails struct {
21
+ ID uint64 `gorm:"primaryKey" json:"id"`
22
+ AccountID uint `json:"account_id"`
23
+ InitialName string `json:"initial_name"`
24
+ FullName *string `json:"full_name"`
25
+ DateOfBirth *time.Time `json:"date_of_birth"`
26
+ PlaceOfBirth *string `json:"place_of_birth"`
27
+ Domicile *string `json:"domicile"`
28
+ LastJob *string `json:"last_job"`
29
+ Gender *string `json:"gender"`
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 {
37
+ ID uint `gorm:"primaryKey" json:"id"`
38
+ UUID uuid.UUID `gorm:"type:uuid" json:"uuid" `
39
+ Token uint `json:"token"`
40
+ AccountID uint `json:"account_id"`
41
+ IsExpired bool `json:"is_expired"`
42
+ CreatedAt time.Time `json:"created_at"`
43
+ ExpiredAt time.Time `json:"expired_at"`
44
+ }
45
+
46
+ type ExternalAuth struct {
47
+ ID uint `gorm:"primaryKey" json:"id"`
48
+ UUID uuid.UUID `gorm:"type:uuid" json:"uuid" `
49
+ OauthID string `json:"oauth_id"`
50
+ AccountID uint `json:"account_id"`
51
+ OauthProvider string `json:"oauth_provider"`
52
+ }
53
+
54
+ type FCM struct {
55
+ ID uint `gorm:"primaryKey" json:"id"`
56
+ AccountID uint `json:"account_id"`
57
+ FCMToken string `json:"fcm_token"`
58
+ }
59
+
60
+ type ForgotPassword struct {
61
+ ID uint `gorm:"primaryKey" json:"id"`
62
+ UUID uuid.UUID `gorm:"type:uuid" json:"uuid" `
63
+ Token uint `json:"token"`
64
+ AccountID uint `json:"account_id"`
65
+ IsExpired bool `json:"is_expired"`
66
+ CreatedAt time.Time `json:"created_at"`
67
+ ExpiredAt time.Time `json:"expired_at"`
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" 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
+
82
+ type AcademyMaterial struct {
83
+ ID uint `gorm:"primaryKey" json:"id"`
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
+ }
91
+
92
+ type AcademyContent struct {
93
+ ID uint `gorm:"primaryKey" json:"id"`
94
+ UUID uuid.UUID `json:"uuid"`
95
+ Title string `json:"title"`
96
+ Order uint `json:"order"`
97
+ AcademyMaterialID uint `json:"academy_material_id"`
98
+ Description string `json:"description"`
99
+ }
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 {
107
+ ID uint `gorm:"primaryKey" json:"id"`
108
+ OptionCategoryID uint `json:"option_category_id"`
109
+ OptionValue string `json:"option_value"`
110
+ }
111
+ type AcademyMaterialProgress struct {
112
+ ID uint `gorm:"primaryKey" json:"id"`
113
+ UUID uuid.UUID `gorm:"type:uuid" json:"uuid"`
114
+ AccountID uint `json:"account_id"`
115
+ AcademyMaterialID uint `json:"academy_material_id"`
116
+ Progress uint `json:"progress"`
117
+ }
118
+
119
+ type AcademyContentProgress struct {
120
+ ID uint `gorm:"primaryKey" json:"id"`
121
+ UUID uuid.UUID `gorm:"type:uuid" json:"uuid"`
122
+ AccountID uint `json:"account_id"`
123
+ AcademyID uint `json:"academy_id"`
124
+ }
125
+
126
+ type RegionProvince struct {
127
+ ID uint `json:"id"`
128
+ Name string `json:"name"`
129
+ Code string `json:"code"`
130
+ }
131
+
132
+ type RegionCity struct {
133
+ ID uint `json:"id"`
134
+ Type string `json:"type"`
135
+ Name string `json:"name"`
136
+ Code string `json:"code"`
137
+ FullCode string `json:"full_code"`
138
+ ProvinceID uint `json:"province_id"`
139
+ }
140
+ type Answer struct {
141
+ ID uint `gorm:"primaryKey" json:"id"`
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" json:"id"`
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" json:"id"`
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" json:"id"`
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
+ type QuizResult struct {
181
+ QuizAttemptID uint `gorm:"column:quiz_attempt_id" json:"quiz_attempt_id"`
182
+ TotalQuestions int `gorm:"column:total_questions" json:"total_questions"`
183
+ CorrectAnswers int `gorm:"column:correct_answers" json:"correct_answers"`
184
+ AverageScore float64 `gorm:"column:average_score" json:"average_score"`
185
+ }
186
+
187
+ // Gorm table name settings
188
+ func (Account) TableName() string { return "account" }
189
+ func (AccountDetails) TableName() string { return "account_details" }
190
+ func (EmailVerification) TableName() string { return "email_verifications" }
191
+ func (ExternalAuth) TableName() string { return "extern_auth" }
192
+ func (FCM) TableName() string { return "fcm" }
193
+ func (ForgotPassword) TableName() string { return "forgot_password" }
194
+ func (Academy) TableName() string { return "academy" }
195
+ func (AcademyMaterial) TableName() string { return "academy_materials" }
196
+ func (AcademyContent) TableName() string { return "academy_contents" }
197
+ func (AcademyMaterialProgress) TableName() string { return "academy_materials_progress" }
198
+ func (AcademyContentProgress) TableName() string { return "academy_contents_progress" }
199
+ func (RegionProvince) TableName() string { return "region_provinces" }
200
+ func (RegionCity) TableName() string { return "region_cities" }
201
+ func (Answer) TableName() string { return "answers" }
202
+ func (Question) TableName() string { return "questions" }
203
+ func (Quiz) TableName() string { return "quizzes" }
204
+ func (QuizAttempt) TableName() string { return "quiz_attempts" }
205
+ func (UserAnswer) TableName() string { return "user_answers" }
206
+ func (OptionCategory) TableName() string { return "option_categories" }
207
+ func (OptionValues) TableName() string { return "option_values" }
space/models/exception_model.go CHANGED
@@ -1,17 +1,23 @@
1
  package models
2
 
3
  type Exception struct {
4
- Unauthorized bool `json:"unauthorized,omitempty"`
5
- BadRequest bool `json:"bad_request,omitempty"`
6
- DataNotFound bool `json:"data_not_found,omitempty"`
7
- InternalServerError bool `json:"internal_server_error,omitempty"`
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
- Forbidden bool `json:"forbidden,omitempty"`
15
- ValidationError bool `json:"validation_error,omitempty"`
16
- Message string `json:"message,omitempty"`
 
 
 
 
 
 
17
  }
 
1
  package models
2
 
3
  type Exception struct {
4
+ Unauthorized bool `json:"unauthorized,omitempty"`
5
+ BadRequest bool `json:"bad_request,omitempty"`
6
+ DataNotFound bool `json:"data_not_found,omitempty"`
7
+ InternalServerError bool `json:"internal_server_error,omitempty"`
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
+ Forbidden bool `json:"forbidden,omitempty"`
15
+ ValidationError bool `json:"validation_error,omitempty"`
16
+
17
+ Message string `json:"message,omitempty"`
18
+ Err error `json:"-"`
19
+ }
20
+
21
+ func (a Exception) Error() string {
22
+ return a.Err.Error()
23
  }
space/models/health_check_model.go CHANGED
@@ -1,8 +1,6 @@
1
  package models
2
 
3
- type HealthCheckRequest struct {
4
- ExampleCallback func() (string, string)
5
- }
6
 
7
  type HealthCheckResponse struct {
8
  DatabaseStatus string `json:"database_status"`
 
1
  package models
2
 
3
+ type HealthCheckRequest struct{}
 
 
4
 
5
  type HealthCheckResponse struct {
6
  DatabaseStatus string `json:"database_status"`
space/repositories/health_check_repository.go CHANGED
@@ -7,29 +7,25 @@ import (
7
  "gorm.io/gorm"
8
  )
9
 
10
- type HealthCheckRepository struct {
 
 
 
 
11
  db *gorm.DB
12
  }
13
 
14
- func NewHealthCheckRepository(db *gorm.DB) *HealthCheckRepository {
15
- return &HealthCheckRepository{
16
  db: db,
17
  }
18
  }
19
 
20
- func (r *HealthCheckRepository) Check(ctx context.Context, req *models.HealthCheckRequest) (*models.HealthCheckResponse, error) {
21
- res := &models.HealthCheckResponse{}
22
-
23
  err := config.RunTx(ctx, r.db, func(tx *gorm.DB) error {
24
  if err := tx.Exec("SELECT 1").Error; err != nil {
25
  return err
26
  }
27
-
28
- // call logic from service if needed
29
- // instead of writing the logic in the repository
30
- // and make sure the param consist of the ctx and request
31
- res.DatabaseStatus, res.RedisStatus = req.ExampleCallback()
32
-
33
  return nil
34
  })
35
 
@@ -37,5 +33,8 @@ func (r *HealthCheckRepository) Check(ctx context.Context, req *models.HealthChe
37
  return nil, err
38
  }
39
 
40
- return res, nil
 
 
 
41
  }
 
7
  "gorm.io/gorm"
8
  )
9
 
10
+ type HealthCheckRepository interface {
11
+ Check(ctx context.Context, req *models.HealthCheckRequest) (*models.HealthCheckResponse, error)
12
+ }
13
+
14
+ type healthCheckRepository struct {
15
  db *gorm.DB
16
  }
17
 
18
+ func NewHealthCheckRepository(db *gorm.DB) HealthCheckRepository {
19
+ return &healthCheckRepository{
20
  db: db,
21
  }
22
  }
23
 
24
+ func (r *healthCheckRepository) Check(ctx context.Context, req *models.HealthCheckRequest) (*models.HealthCheckResponse, error) {
 
 
25
  err := config.RunTx(ctx, r.db, func(tx *gorm.DB) error {
26
  if err := tx.Exec("SELECT 1").Error; err != nil {
27
  return err
28
  }
 
 
 
 
 
 
29
  return nil
30
  })
31
 
 
33
  return nil, err
34
  }
35
 
36
+ return &models.HealthCheckResponse{
37
+ DatabaseStatus: "OK",
38
+ RedisStatus: "OK",
39
+ }, nil
40
  }
space/response/api_response_v2.go ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package response
2
+
3
+ import (
4
+ "api.qobiltu.id/models"
5
+ "api.qobiltu.id/utils"
6
+ "errors"
7
+ "net/http"
8
+
9
+ "github.com/gin-gonic/gin"
10
+ )
11
+
12
+ func HandleError(c *gin.Context, err error) {
13
+ var exception models.Exception
14
+
15
+ if errors.As(err, &exception) {
16
+ utils.LogError(exception.Err)
17
+
18
+ switch {
19
+ case exception.DataDuplicate:
20
+ responseError(c, http.StatusConflict, exception)
21
+ case exception.Unauthorized:
22
+ responseError(c, http.StatusUnauthorized, exception)
23
+ case exception.DataNotFound:
24
+ responseError(c, http.StatusNotFound, exception)
25
+ case exception.Forbidden:
26
+ responseError(c, http.StatusForbidden, exception)
27
+ case exception.BadRequest:
28
+ responseError(c, http.StatusBadRequest, exception)
29
+ case exception.InternalServerError:
30
+ responseError(c, http.StatusInternalServerError, exception)
31
+ case exception.QueryError:
32
+ responseError(c, http.StatusInternalServerError, exception)
33
+ case exception.InvalidPasswordLength:
34
+ responseError(c, http.StatusBadRequest, exception)
35
+ case exception.IsPassTheLimit:
36
+ responseError(c, http.StatusTooManyRequests, exception)
37
+ case exception.IsTimeOut:
38
+ responseError(c, http.StatusRequestTimeout, exception)
39
+ case exception.AttemptNotFound:
40
+ responseError(c, http.StatusNotFound, exception)
41
+ case exception.ValidationError:
42
+ responseError(c, http.StatusUnprocessableEntity, exception)
43
+ default:
44
+ responseError(c, http.StatusInternalServerError, exception)
45
+ }
46
+ } else {
47
+ utils.LogError(err)
48
+ responseError(c, http.StatusInternalServerError, models.Exception{
49
+ InternalServerError: true,
50
+ Message: "Internal Server Error",
51
+ })
52
+ }
53
+ }
54
+
55
+ func HandleSuccess(c *gin.Context, status int, msg string, data any, metaData any) {
56
+ res := models.SuccessResponse{
57
+ Status: "success",
58
+ Message: msg,
59
+ Data: data,
60
+ MetaData: metaData,
61
+ }
62
+
63
+ c.JSON(status, res)
64
+ return
65
+ }
66
+
67
+ func responseError(c *gin.Context, status int, exception models.Exception) {
68
+ message := exception.Message
69
+ exception.Message = ""
70
+
71
+ res := models.ErrorResponse{
72
+ Status: "error",
73
+ Message: message,
74
+ Errors: exception,
75
+ }
76
+
77
+ c.AbortWithStatusJSON(status, res)
78
+ return
79
+ }
space/response/gorm.go ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package response
2
+
3
+ import (
4
+ "api.qobiltu.id/models"
5
+ "errors"
6
+ "strings"
7
+
8
+ "gorm.io/gorm"
9
+ )
10
+
11
+ func HandleGormError(err error, fallbackMessage string) error {
12
+ if err == nil {
13
+ return nil
14
+ }
15
+
16
+ if errors.Is(err, gorm.ErrRecordNotFound) {
17
+ return models.Exception{
18
+ Message: "Data not found",
19
+ DataNotFound: true,
20
+ Err: err,
21
+ }
22
+ }
23
+
24
+ lowerErr := strings.ToLower(err.Error())
25
+ if strings.Contains(lowerErr, "duplicated key") || strings.Contains(lowerErr, "unique constraint") || strings.Contains(lowerErr, "duplicate entry") {
26
+ return models.Exception{
27
+ Message: "Data already exists",
28
+ DataDuplicate: true,
29
+ Err: err,
30
+ }
31
+ }
32
+
33
+ if strings.Contains(lowerErr, "password") && strings.Contains(lowerErr, "length") {
34
+ return models.Exception{
35
+ Message: "Invalid password length",
36
+ InvalidPasswordLength: true,
37
+ Err: err,
38
+ }
39
+ }
40
+
41
+ if strings.Contains(lowerErr, "permission denied") || strings.Contains(lowerErr, "forbidden") {
42
+ return models.Exception{
43
+ Message: "Access forbidden",
44
+ Forbidden: true,
45
+ Err: err,
46
+ }
47
+ }
48
+
49
+ if errors.As(err, &gorm.ErrInvalidData) {
50
+ return models.Exception{
51
+ Message: "Invalid data format",
52
+ BadRequest: true,
53
+ Err: err,
54
+ }
55
+ }
56
+
57
+ return models.Exception{
58
+ Message: fallbackMessage,
59
+ InternalServerError: true,
60
+ Err: err,
61
+ }
62
+ }
space/router/server.go CHANGED
@@ -7,11 +7,11 @@ import (
7
 
8
  type Server struct {
9
  router *gin.Engine
10
- healthCheckController *health_check_controller.HealthCheckController
11
  }
12
 
13
  func NewServer(
14
- healthCheckController *health_check_controller.HealthCheckController,
15
  ) (*Server, error) {
16
 
17
  router := gin.Default()
 
7
 
8
  type Server struct {
9
  router *gin.Engine
10
+ healthCheckController health_check_controller.HealthCheckController
11
  }
12
 
13
  func NewServer(
14
+ healthCheckController health_check_controller.HealthCheckController,
15
  ) (*Server, error) {
16
 
17
  router := gin.Default()
space/services/health_check_service.go ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package services
2
+
3
+ import (
4
+ "api.qobiltu.id/models"
5
+ "api.qobiltu.id/repositories"
6
+ "api.qobiltu.id/response"
7
+ "context"
8
+ "gorm.io/gorm"
9
+ )
10
+
11
+ type HealthCheckService interface {
12
+ Check(ctx context.Context, req *models.HealthCheckRequest) (*models.HealthCheckResponse, error)
13
+ }
14
+
15
+ type healthCheckService struct {
16
+ healthCheckRepository repositories.HealthCheckRepository
17
+ }
18
+
19
+ func NewHealthCheckService(healthCheckRepository repositories.HealthCheckRepository) HealthCheckService {
20
+ return &healthCheckService{
21
+ healthCheckRepository: healthCheckRepository,
22
+ }
23
+ }
24
+
25
+ func (s *healthCheckService) Check(ctx context.Context, req *models.HealthCheckRequest) (*models.HealthCheckResponse, error) {
26
+ res, err := s.healthCheckRepository.Check(ctx, req)
27
+ if err != nil {
28
+ return nil, response.HandleGormError(gorm.ErrDuplicatedKey, "Internal Server Error")
29
+ }
30
+
31
+ return res, nil
32
+ }
space/services/user_profile_service.go CHANGED
@@ -1,115 +1,115 @@
1
- package services
2
-
3
- import (
4
- "regexp"
5
- "strconv"
6
- "strings"
7
-
8
- "api.qobiltu.id/models"
9
- "api.qobiltu.id/repositories"
10
- )
11
-
12
- type UserProfileService struct {
13
- Service[models.AccountDetails, models.UserProfileResponse]
14
- }
15
-
16
- // SanitizePhoneNumber membersihkan dan menormalkan nomor telepon ke format +62
17
- func SanitizePhoneNumber(input string) string {
18
- // Hilangkan semua spasi dan strip
19
- input = strings.ReplaceAll(input, " ", "")
20
- input = strings.ReplaceAll(input, "-", "")
21
- input = strings.ReplaceAll(input, "(", "")
22
- input = strings.ReplaceAll(input, ")", "")
23
-
24
- // Hilangkan semua karakter non-digit kecuali +
25
- re := regexp.MustCompile(`[^0-9\+]`)
26
- input = re.ReplaceAllString(input, "")
27
-
28
- // Handle nomor diawali 0 (contoh: 0812...) menjadi +62812...
29
- if strings.HasPrefix(input, "0") {
30
- input = "+62" + input[1:]
31
- }
32
-
33
- // Handle jika diawali dengan 62 tanpa + (contoh: 62812...)
34
- if strings.HasPrefix(input, "62") && !strings.HasPrefix(input, "+62") {
35
- input = "+" + input
36
- }
37
-
38
- // Handle jika tidak ada awalan +62 sama sekali (contoh: 8123456789)
39
- if !strings.HasPrefix(input, "+62") {
40
- if strings.HasPrefix(input, "8") {
41
- input = "+62" + input
42
- }
43
- }
44
-
45
- return input
46
- }
47
- func (s *UserProfileService) Create() {
48
- userProfile := repositories.CreateAccountDetails(s.Constructor)
49
- s.Error = userProfile.RowsError
50
- if userProfile.NoRecord {
51
- s.Exception.DataNotFound = true
52
- s.Exception.Message = "There is no account with given credentials!"
53
- return
54
- }
55
- s.Result = models.UserProfileResponse{
56
- Account: repositories.GetAccountById(s.Constructor.AccountID).Result,
57
- Details: userProfile.Result,
58
- }
59
- }
60
- func (s *UserProfileService) Retrieve() {
61
- userProfile := repositories.GetDetailAccountById(s.Constructor.AccountID)
62
- s.Error = userProfile.RowsError
63
- if userProfile.NoRecord {
64
- s.Exception.DataNotFound = true
65
- s.Exception.Message = "There is no account with given credentials!"
66
- return
67
- }
68
- s.Result = models.UserProfileResponse{
69
- Account: repositories.GetAccountById(s.Constructor.AccountID).Result,
70
- Details: userProfile.Result,
71
- }
72
- s.Result.Account.Password = "SECRET"
73
- }
74
-
75
- func (s *UserProfileService) Update() {
76
- if s.Constructor.PhoneNumber != nil {
77
- phoneNumber := *s.Constructor.PhoneNumber
78
- *s.Constructor.PhoneNumber = SanitizePhoneNumber(phoneNumber)
79
- }
80
- usersCount := repositories.GetAllAccount().RowsCount
81
- var initialName string
82
- if s.Constructor.Gender != nil {
83
- if *s.Constructor.Gender {
84
- initialName = "IKH_"
85
- } else {
86
- initialName = "AKH_"
87
- }
88
- }
89
-
90
- initialName += strconv.Itoa(usersCount)
91
- s.Constructor.InitialName = initialName
92
- userProfile := repositories.UpdateAccountDetails(s.Constructor)
93
- s.Error = userProfile.RowsError
94
- if userProfile.NoRecord {
95
- s.Exception.DataNotFound = true
96
- s.Exception.Message = "There is no account with given credentials!"
97
- return
98
- }
99
- account := repositories.GetAccountById(s.Constructor.AccountID)
100
- account.Result.IsDetailCompleted = (userProfile.Result.InitialName != "" &&
101
- userProfile.Result.FullName != nil &&
102
- userProfile.Result.DateOfBirth != nil &&
103
- userProfile.Result.PlaceOfBirth != nil &&
104
- userProfile.Result.Domicile != nil &&
105
- userProfile.Result.LastJob != nil &&
106
- userProfile.Result.Gender != nil &&
107
- userProfile.Result.LastEducation != nil &&
108
- userProfile.Result.MaritalStatus != nil)
109
- repositories.UpdateAccount(account.Result)
110
- s.Result = models.UserProfileResponse{
111
- Account: account.Result,
112
- Details: userProfile.Result,
113
- }
114
- s.Result.Account.Password = "SECRET"
115
- }
 
1
+ package services
2
+
3
+ import (
4
+ "regexp"
5
+ "strconv"
6
+ "strings"
7
+
8
+ "api.qobiltu.id/models"
9
+ "api.qobiltu.id/repositories"
10
+ )
11
+
12
+ type UserProfileService struct {
13
+ Service[models.AccountDetails, models.UserProfileResponse]
14
+ }
15
+
16
+ // SanitizePhoneNumber membersihkan dan menormalkan nomor telepon ke format +62
17
+ func SanitizePhoneNumber(input string) string {
18
+ // Hilangkan semua spasi dan strip
19
+ input = strings.ReplaceAll(input, " ", "")
20
+ input = strings.ReplaceAll(input, "-", "")
21
+ input = strings.ReplaceAll(input, "(", "")
22
+ input = strings.ReplaceAll(input, ")", "")
23
+
24
+ // Hilangkan semua karakter non-digit kecuali +
25
+ re := regexp.MustCompile(`[^0-9\+]`)
26
+ input = re.ReplaceAllString(input, "")
27
+
28
+ // Handle nomor diawali 0 (contoh: 0812...) menjadi +62812...
29
+ if strings.HasPrefix(input, "0") {
30
+ input = "+62" + input[1:]
31
+ }
32
+
33
+ // Handle jika diawali dengan 62 tanpa + (contoh: 62812...)
34
+ if strings.HasPrefix(input, "62") && !strings.HasPrefix(input, "+62") {
35
+ input = "+" + input
36
+ }
37
+
38
+ // Handle jika tidak ada awalan +62 sama sekali (contoh: 8123456789)
39
+ if !strings.HasPrefix(input, "+62") {
40
+ if strings.HasPrefix(input, "8") {
41
+ input = "+62" + input
42
+ }
43
+ }
44
+
45
+ return input
46
+ }
47
+ func (s *UserProfileService) Create() {
48
+ userProfile := repositories.CreateAccountDetails(s.Constructor)
49
+ s.Error = userProfile.RowsError
50
+ if userProfile.NoRecord {
51
+ s.Exception.DataNotFound = true
52
+ s.Exception.Message = "There is no account with given credentials!"
53
+ return
54
+ }
55
+ s.Result = models.UserProfileResponse{
56
+ Account: repositories.GetAccountById(s.Constructor.AccountID).Result,
57
+ Details: userProfile.Result,
58
+ }
59
+ }
60
+ func (s *UserProfileService) Retrieve() {
61
+ userProfile := repositories.GetDetailAccountById(s.Constructor.AccountID)
62
+ s.Error = userProfile.RowsError
63
+ if userProfile.NoRecord {
64
+ s.Exception.DataNotFound = true
65
+ s.Exception.Message = "There is no account with given credentials!"
66
+ return
67
+ }
68
+ s.Result = models.UserProfileResponse{
69
+ Account: repositories.GetAccountById(s.Constructor.AccountID).Result,
70
+ Details: userProfile.Result,
71
+ }
72
+ s.Result.Account.Password = "SECRET"
73
+ }
74
+
75
+ func (s *UserProfileService) Update() {
76
+ if s.Constructor.PhoneNumber != nil {
77
+ phoneNumber := *s.Constructor.PhoneNumber
78
+ *s.Constructor.PhoneNumber = SanitizePhoneNumber(phoneNumber)
79
+ }
80
+ usersCount := repositories.GetAllAccount().RowsCount
81
+ var initialName string
82
+ if s.Constructor.Gender != nil {
83
+ if strings.ToLower(*s.Constructor.Gender) == "laki-laki" {
84
+ initialName = "IKH_"
85
+ } else {
86
+ initialName = "AKH_"
87
+ }
88
+ }
89
+
90
+ initialName += strconv.Itoa(usersCount)
91
+ s.Constructor.InitialName = initialName
92
+ userProfile := repositories.UpdateAccountDetails(s.Constructor)
93
+ s.Error = userProfile.RowsError
94
+ if userProfile.NoRecord {
95
+ s.Exception.DataNotFound = true
96
+ s.Exception.Message = "There is no account with given credentials!"
97
+ return
98
+ }
99
+ account := repositories.GetAccountById(s.Constructor.AccountID)
100
+ account.Result.IsDetailCompleted = (userProfile.Result.InitialName != "" &&
101
+ userProfile.Result.FullName != nil &&
102
+ userProfile.Result.DateOfBirth != nil &&
103
+ userProfile.Result.PlaceOfBirth != nil &&
104
+ userProfile.Result.Domicile != nil &&
105
+ userProfile.Result.LastJob != nil &&
106
+ userProfile.Result.Gender != nil &&
107
+ userProfile.Result.LastEducation != nil &&
108
+ userProfile.Result.MaritalStatus != nil)
109
+ repositories.UpdateAccount(account.Result)
110
+ s.Result = models.UserProfileResponse{
111
+ Account: account.Result,
112
+ Details: userProfile.Result,
113
+ }
114
+ s.Result.Account.Password = "SECRET"
115
+ }
space/space/Dockerfile CHANGED
@@ -25,6 +25,12 @@ FROM alpine:latest
25
  # Set working directory
26
  WORKDIR /app
27
 
 
 
 
 
 
 
28
  # Copy hasil build dari builder ke image runtime
29
  COPY --from=builder /app/main .
30
 
 
25
  # Set working directory
26
  WORKDIR /app
27
 
28
+ # Install tzdata
29
+ RUN apk update && apk add --no-cache tzdata
30
+
31
+ # Set the timezone environment variable
32
+ ENV TZ="Asia/Jakarta"
33
+
34
  # Copy hasil build dari builder ke image runtime
35
  COPY --from=builder /app/main .
36
 
space/space/space/.github/workflows/main.yml CHANGED
@@ -25,4 +25,5 @@ jobs:
25
  git pull origin main
26
  docker-compose down -v
27
  docker-compose up --build -d
 
28
  EOF
 
25
  git pull origin main
26
  docker-compose down -v
27
  docker-compose up --build -d
28
+ docker image prune -f
29
  EOF
space/space/space/apperror/apperror.go ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package apperror
2
+
3
+ import (
4
+ "api.qobiltu.id/validation"
5
+ "fmt"
6
+ )
7
+
8
+ // AppError is a custom error type for the application.
9
+ type AppError struct {
10
+ Code string // Optional: Error code for programmatic handling
11
+ Message string // Human-readable error message
12
+ Err error // Underlying error, if any
13
+ Details map[string]any // Optional: Additional details about the error
14
+ }
15
+
16
+ func (e AppError) Error() string {
17
+ if e.Code != "" {
18
+ return fmt.Sprintf("[%s] %s", e.Code, e.Message)
19
+ }
20
+ return e.Message
21
+ }
22
+
23
+ // NewAppError creates a new AppError.
24
+ func NewAppError(code, message string, err error, details map[string]any) error {
25
+ return &AppError{
26
+ Code: code,
27
+ Message: message,
28
+ Err: err,
29
+ Details: details,
30
+ }
31
+ }
32
+
33
+ // ValidationError represents errors related to data validation.
34
+ type ValidationError struct {
35
+ Errors []validation.ErrorMessage // Structure to hold field and message of validation errors
36
+ }
37
+
38
+ func (e ValidationError) Error() string {
39
+ return fmt.Sprintf("validation failed: %+v", e.Errors)
40
+ }
41
+
42
+ // NewValidationError creates a new ValidationError.
43
+ func NewValidationError(message string, errors []validation.ErrorMessage) error {
44
+ return &AppError{
45
+ Code: "VALIDATION_ERROR",
46
+ Message: message,
47
+ Err: ValidationError{Errors: errors},
48
+ }
49
+ }
50
+
51
+ // InternalError represents unexpected errors within the application.
52
+ type InternalError struct {
53
+ Message string
54
+ Err error
55
+ }
56
+
57
+ func (e InternalError) Error() string {
58
+ return fmt.Sprintf("internal server error: %v", e.Err)
59
+ }
60
+
61
+ // NewInternalError creates a new InternalError wrapped in AppError.
62
+ func NewInternalError(message string, err error) error {
63
+ return &AppError{
64
+ Code: "INTERNAL_ERROR",
65
+ Message: message,
66
+ Err: InternalError{Message: message, Err: err},
67
+ }
68
+ }
69
+
70
+ // ConflictError represents errors due to a conflict with the current state.
71
+ type ConflictError struct {
72
+ Message string
73
+ Err error
74
+ }
75
+
76
+ func (e ConflictError) Error() string {
77
+ return fmt.Sprintf("conflict: %s", e.Err)
78
+ }
79
+
80
+ // NewConflictError creates a new ConflictError wrapped in AppError.
81
+ func NewConflictError(message string, err error) error {
82
+ return &AppError{
83
+ Code: "CONFLICT_ERROR",
84
+ Message: message,
85
+ Err: ConflictError{Message: message, Err: err},
86
+ }
87
+ }
88
+
89
+ // NotFoundError represents errors when a resource is not found.
90
+ type NotFoundError struct {
91
+ Message string
92
+ Resource string
93
+ ID any
94
+ }
95
+
96
+ func (e NotFoundError) Error() string {
97
+ return fmt.Sprintf("%s with ID '%v' not found", e.Resource, e.ID)
98
+ }
99
+
100
+ // NewNotFoundError creates a new NotFoundError wrapped in AppError.
101
+ func NewNotFoundError(resource string, id any) error {
102
+ message := fmt.Sprintf("%s not found", resource)
103
+ return &AppError{
104
+ Code: "NOT_FOUND_ERROR",
105
+ Message: fmt.Sprintf("%s not found", resource),
106
+ Err: NotFoundError{Message: message, Resource: resource, ID: id},
107
+ Details: map[string]any{
108
+ "resource": resource,
109
+ "id": id,
110
+ },
111
+ }
112
+ }
113
+
114
+ // UnauthorizedError represents errors when access is denied due to lack of credentials.
115
+ type UnauthorizedError struct {
116
+ Message string
117
+ Err error
118
+ }
119
+
120
+ func (e UnauthorizedError) Error() string {
121
+ return fmt.Sprintf("unauthorized: %s", e.Err)
122
+ }
123
+
124
+ // NewUnauthorizedError creates a new UnauthorizedError wrapped in AppError.
125
+ func NewUnauthorizedError(message string, err error) error {
126
+ return &AppError{
127
+ Code: "UNAUTHORIZED_ERROR",
128
+ Message: message,
129
+ Err: UnauthorizedError{Message: message, Err: err},
130
+ }
131
+ }
132
+
133
+ // ForbiddenError represents errors when access is denied even with valid credentials.
134
+ type ForbiddenError struct {
135
+ Message string
136
+ Err error
137
+ }
138
+
139
+ func (e ForbiddenError) Error() string {
140
+ return fmt.Sprintf("forbidden: %s", e.Err)
141
+ }
142
+
143
+ // NewForbiddenError creates a new ForbiddenError wrapped in AppError.
144
+ func NewForbiddenError(message string, err error) error {
145
+ return &AppError{
146
+ Code: "FORBIDDEN_ERROR",
147
+ Message: message,
148
+ Err: ForbiddenError{Message: message, Err: err},
149
+ }
150
+ }
151
+
152
+ // BadRequestError represents errors due to an invalid request.
153
+ type BadRequestError struct {
154
+ Message string
155
+ Err error
156
+ }
157
+
158
+ func (e BadRequestError) Error() string {
159
+ return fmt.Sprintf("bad request: %s", e.Err)
160
+ }
161
+
162
+ func NewBadRequestError(message string, err error) error {
163
+ return &AppError{
164
+ Code: "BAD_REQUEST_ERROR",
165
+ Message: message,
166
+ Err: BadRequestError{Message: message, Err: err},
167
+ }
168
+ }
space/space/space/config/database_connection_config.go CHANGED
@@ -1,79 +1,80 @@
1
- package config
2
-
3
- import (
4
- "fmt"
5
- "log"
6
- "os"
7
-
8
- "gorm.io/driver/postgres"
9
- "gorm.io/gorm"
10
- "gorm.io/gorm/logger"
11
-
12
- "api.qobiltu.id/models"
13
- "github.com/joho/godotenv"
14
- )
15
-
16
- var DB *gorm.DB
17
- var err error
18
- var Salt string
19
-
20
- func init() {
21
- godotenv.Load()
22
- if err != nil {
23
- fmt.Println("Gagal membaca file .env")
24
- return
25
- }
26
- os.Setenv("TZ", "Asia/Jakarta")
27
- dbHost := os.Getenv("DB_HOST")
28
- dbPort := os.Getenv("DB_PORT")
29
- dbUser := os.Getenv("DB_USER")
30
- dbPassword := os.Getenv("DB_PASSWORD")
31
- dbName := os.Getenv("DB_NAME")
32
- Salt := os.Getenv("SALT")
33
- dsn := "host=" + dbHost + " user=" + dbUser + " password=" + dbPassword + " dbname=" + dbName + " port=" + dbPort + " sslmode=disable TimeZone=Asia/Jakarta"
34
- DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{TranslateError: true})
35
- if err != nil {
36
- panic(err)
37
- }
38
- if Salt == "" {
39
- Salt = "D3f4u|t"
40
- }
41
-
42
- // Call AutoMigrateAll to perform auto-migration
43
- AutoMigrateAll(DB)
44
- }
45
-
46
- func AutoMigrateAll(db *gorm.DB) {
47
- // Enable logger to see SQL logs
48
- db.Logger.LogMode(logger.Info)
49
-
50
- // Auto-migrate all models
51
- err := db.AutoMigrate(
52
- &models.Account{},
53
- &models.AccountDetails{},
54
- &models.EmailVerification{},
55
- &models.ExternalAuth{},
56
- &models.FCM{},
57
- &models.ForgotPassword{},
58
- &models.Academy{},
59
- &models.AcademyMaterial{},
60
- &models.AcademyContent{},
61
- &models.AcademyMaterialProgress{},
62
- &models.AcademyContentProgress{},
63
- &models.RegionCity{},
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 {
75
- log.Fatal(err)
76
- }
77
-
78
- fmt.Println("Migration completed successfully.")
79
- }
 
 
1
+ package config
2
+
3
+ import (
4
+ "fmt"
5
+ "log"
6
+ "log/slog"
7
+ "os"
8
+
9
+ "gorm.io/driver/postgres"
10
+ "gorm.io/gorm"
11
+ "gorm.io/gorm/logger"
12
+
13
+ "api.qobiltu.id/models"
14
+ "github.com/joho/godotenv"
15
+ )
16
+
17
+ var DB *gorm.DB
18
+ var err error
19
+ var Salt string
20
+
21
+ func init() {
22
+ godotenv.Load()
23
+ if err != nil {
24
+ fmt.Println("Gagal membaca file .env")
25
+ return
26
+ }
27
+ os.Setenv("TZ", "Asia/Jakarta")
28
+ dbHost := os.Getenv("DB_HOST")
29
+ dbPort := os.Getenv("DB_PORT")
30
+ dbUser := os.Getenv("DB_USER")
31
+ dbPassword := os.Getenv("DB_PASSWORD")
32
+ dbName := os.Getenv("DB_NAME")
33
+ Salt := os.Getenv("SALT")
34
+ dsn := "host=" + dbHost + " user=" + dbUser + " password=" + dbPassword + " dbname=" + dbName + " port=" + dbPort + " sslmode=disable TimeZone=Asia/Jakarta"
35
+ DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{TranslateError: true})
36
+ if err != nil {
37
+ panic(err)
38
+ }
39
+ if Salt == "" {
40
+ Salt = "D3f4u|t"
41
+ }
42
+
43
+ // Call AutoMigrateAll to perform auto-migration
44
+ AutoMigrateAll(DB)
45
+ }
46
+
47
+ func AutoMigrateAll(db *gorm.DB) {
48
+ // Enable logger to see SQL logs
49
+ db.Logger.LogMode(logger.Info)
50
+
51
+ // Auto-migrate all models
52
+ err := db.AutoMigrate(
53
+ &models.Account{},
54
+ &models.AccountDetails{},
55
+ &models.EmailVerification{},
56
+ &models.ExternalAuth{},
57
+ &models.FCM{},
58
+ &models.ForgotPassword{},
59
+ &models.Academy{},
60
+ &models.AcademyMaterial{},
61
+ &models.AcademyContent{},
62
+ &models.AcademyMaterialProgress{},
63
+ &models.AcademyContentProgress{},
64
+ &models.RegionCity{},
65
+ &models.RegionProvince{},
66
+ &models.OptionCategory{},
67
+ &models.OptionValues{},
68
+ &models.Quiz{},
69
+ &models.QuizAttempt{},
70
+ &models.Question{},
71
+ &models.Answer{},
72
+ &models.UserAnswer{},
73
+ )
74
+
75
+ if err != nil {
76
+ log.Fatal(err)
77
+ }
78
+
79
+ slog.Info("Auto-migration completed successfully")
80
+ }
space/space/space/config/tx.go ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package config
2
+
3
+ import (
4
+ "context"
5
+ "fmt"
6
+ "gorm.io/gorm"
7
+ )
8
+
9
+ func RunTx(ctx context.Context, db *gorm.DB, fn func(tx *gorm.DB) error) error {
10
+ tx := db.WithContext(ctx).Begin()
11
+ if tx.Error != nil {
12
+ return fmt.Errorf("failed to begin transaction: %w", tx.Error)
13
+ }
14
+
15
+ defer func() {
16
+ if p := recover(); p != nil {
17
+ _ = tx.Rollback()
18
+ panic(p) // Re-throw panic setelah rollback
19
+ }
20
+ }()
21
+
22
+ if err := fn(tx); err != nil {
23
+ if rbErr := tx.Rollback().Error; rbErr != nil {
24
+ return fmt.Errorf("transaction error: %v, rollback error: %w", err, rbErr)
25
+ }
26
+ return err
27
+ }
28
+
29
+ if err := tx.Commit().Error; err != nil {
30
+ return fmt.Errorf("failed to commit transaction: %w", err)
31
+ }
32
+
33
+ return nil
34
+ }
space/space/space/controller/health_check/health_check_controller.go ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package health_check_controller
2
+
3
+ import (
4
+ "api.qobiltu.id/models"
5
+ "api.qobiltu.id/response"
6
+ "api.qobiltu.id/services/health_check_service"
7
+ "github.com/gin-gonic/gin"
8
+ "net/http"
9
+ )
10
+
11
+ type HealthCheckController struct {
12
+ healthCheckService *health_check_service.HealthCheckService
13
+ }
14
+
15
+ func NewHealthCheckController(healthCheckService *health_check_service.HealthCheckService) *HealthCheckController {
16
+ return &HealthCheckController{
17
+ healthCheckService: healthCheckService,
18
+ }
19
+ }
20
+
21
+ func (h *HealthCheckController) Check(ctx *gin.Context) {
22
+ req := models.HealthCheckRequest{}
23
+
24
+ res, err := h.healthCheckService.Check(ctx, &req)
25
+ if err != nil {
26
+ response.HandleError(ctx, err)
27
+ return
28
+ }
29
+
30
+ response.HandleSuccess(ctx, "Service is running", http.StatusOK, res, nil)
31
+ }
space/space/space/models/exception_model.go CHANGED
@@ -1,15 +1,17 @@
1
- package models
2
-
3
- type Exception struct {
4
- Unauthorized bool `json:"unauthorized,omitempty"`
5
- BadRequest bool `json:"bad_request,omitempty"`
6
- DataNotFound bool `json:"data_not_found,omitempty"`
7
- InternalServerError bool `json:"internal_server_error,omitempty"`
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
- }
 
 
 
1
+ package models
2
+
3
+ type Exception struct {
4
+ Unauthorized bool `json:"unauthorized,omitempty"`
5
+ BadRequest bool `json:"bad_request,omitempty"`
6
+ DataNotFound bool `json:"data_not_found,omitempty"`
7
+ InternalServerError bool `json:"internal_server_error,omitempty"`
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
+ Forbidden bool `json:"forbidden,omitempty"`
15
+ ValidationError bool `json:"validation_error,omitempty"`
16
+ Message string `json:"message,omitempty"`
17
+ }
space/space/space/models/health_check_model.go ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ package models
2
+
3
+ type HealthCheckRequest struct {
4
+ ExampleCallback func() (string, string)
5
+ }
6
+
7
+ type HealthCheckResponse struct {
8
+ DatabaseStatus string `json:"database_status"`
9
+ RedisStatus string `json:"redis_status"`
10
+ }
space/space/space/repositories/health_check_repository.go ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package repositories
2
+
3
+ import (
4
+ "api.qobiltu.id/config"
5
+ "api.qobiltu.id/models"
6
+ "context"
7
+ "gorm.io/gorm"
8
+ )
9
+
10
+ type HealthCheckRepository struct {
11
+ db *gorm.DB
12
+ }
13
+
14
+ func NewHealthCheckRepository(db *gorm.DB) *HealthCheckRepository {
15
+ return &HealthCheckRepository{
16
+ db: db,
17
+ }
18
+ }
19
+
20
+ func (r *HealthCheckRepository) Check(ctx context.Context, req *models.HealthCheckRequest) (*models.HealthCheckResponse, error) {
21
+ res := &models.HealthCheckResponse{}
22
+
23
+ err := config.RunTx(ctx, r.db, func(tx *gorm.DB) error {
24
+ if err := tx.Exec("SELECT 1").Error; err != nil {
25
+ return err
26
+ }
27
+
28
+ // call logic from service if needed
29
+ // instead of writing the logic in the repository
30
+ // and make sure the param consist of the ctx and request
31
+ res.DatabaseStatus, res.RedisStatus = req.ExampleCallback()
32
+
33
+ return nil
34
+ })
35
+
36
+ if err != nil {
37
+ return nil, err
38
+ }
39
+
40
+ return res, nil
41
+ }
space/space/space/response/paging.go ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package response
2
+
3
+ import (
4
+ "errors"
5
+ "fmt"
6
+ )
7
+
8
+ type PagingInfo struct {
9
+ HasPreviousPage bool `json:"has_previous_page"`
10
+ HasNextPage bool `json:"has_next_page"`
11
+ CurrentPage int `json:"current_page"`
12
+ PerPage int `json:"per_page"`
13
+ TotalData int `json:"total_data"`
14
+ LastPage int `json:"last_page"`
15
+ From int `json:"from"`
16
+ To int `json:"to"`
17
+ TotalDataInCurrentPage int `json:"total_data_in_current_page"`
18
+ Label string `json:"label"`
19
+ }
20
+
21
+ var ErrPageInfo = errors.New("per_page harus lebih besar dari 0 dan offset tidak boleh negatif")
22
+
23
+ func NewPagingInfo(currentPage, perPage, offset, totalData int) (*PagingInfo, error) {
24
+ if perPage <= 0 || offset < 0 {
25
+ return nil, ErrPageInfo
26
+ }
27
+
28
+ lastPage := totalData / perPage
29
+ if totalData%perPage != 0 {
30
+ lastPage++
31
+ }
32
+
33
+ to := min(offset+perPage, totalData)
34
+ from := int(0)
35
+ if to > offset {
36
+ from = offset + 1
37
+ }
38
+
39
+ if currentPage > lastPage {
40
+ currentPage = lastPage
41
+ }
42
+
43
+ totalDataInCurrentPage := to - offset
44
+ label := fmt.Sprintf("Menampilkan %d sampai %d dari %d data", from, to, totalData)
45
+
46
+ return &PagingInfo{
47
+ HasPreviousPage: currentPage > 1,
48
+ HasNextPage: currentPage < lastPage,
49
+ CurrentPage: currentPage,
50
+ PerPage: perPage,
51
+ TotalData: totalData,
52
+ LastPage: lastPage,
53
+ From: from,
54
+ To: to,
55
+ TotalDataInCurrentPage: totalDataInCurrentPage,
56
+ Label: label,
57
+ }, nil
58
+ }
space/space/space/response/response.go ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package response
2
+
3
+ import (
4
+ errors "api.qobiltu.id/apperror"
5
+ "api.qobiltu.id/models"
6
+ goErrors "errors"
7
+ "fmt"
8
+ "github.com/gin-gonic/gin"
9
+ "github.com/go-playground/validator/v10"
10
+ "net/http"
11
+ )
12
+
13
+ type ErrorResponse struct {
14
+ Details string `json:"details"`
15
+ ValidationErrors []ValidationError `json:"validation_errors,omitempty"`
16
+ models.Exception
17
+ }
18
+
19
+ type API struct {
20
+ Status string `json:"status"`
21
+ Message string `json:"message,omitempty"`
22
+ Data any `json:"data,omitempty"`
23
+ MetaData any `json:"meta_data"`
24
+ Error *ErrorResponse `json:"errors,omitempty"`
25
+ }
26
+
27
+ type List struct {
28
+ List any `json:"list"`
29
+ }
30
+
31
+ type ValidationError struct {
32
+ Field string `json:"field"`
33
+ Message string `json:"message"`
34
+ }
35
+
36
+ func HandleError(c *gin.Context, err error) {
37
+
38
+ apiResponse := API{
39
+ Status: "error",
40
+ Message: "An error occurred, " + err.Error(),
41
+ Error: &ErrorResponse{
42
+ Exception: models.Exception{Message: ""},
43
+ },
44
+ MetaData: struct{}{},
45
+ }
46
+
47
+ var appErr *errors.AppError
48
+
49
+ if goErrors.As(err, &appErr) {
50
+ apiResponse.Error.Details = fmt.Sprintf("%v", appErr.Err)
51
+
52
+ switch specificErr := appErr.Err.(type) {
53
+ case errors.ValidationError:
54
+ validationError := make([]ValidationError, len(specificErr.Errors))
55
+ apiResponse.Message = "Validation Error"
56
+ apiResponse.Error.ValidationErrors = validationError
57
+ for i, ve := range specificErr.Errors {
58
+ apiResponse.Error.ValidationErrors[i] = ValidationError{
59
+ Field: ve.Field,
60
+ Message: ve.Message,
61
+ }
62
+ }
63
+ c.JSON(http.StatusBadRequest, apiResponse)
64
+ return
65
+ case errors.ConflictError:
66
+ apiResponse.Message = specificErr.Message
67
+ apiResponse.Error.Details = specificErr.Error()
68
+ apiResponse.Error.Exception.DataDuplicate = true
69
+ c.JSON(http.StatusConflict, apiResponse)
70
+ return
71
+ case errors.InternalError:
72
+ apiResponse.Message = specificErr.Message
73
+ apiResponse.Error.Details = specificErr.Error()
74
+ apiResponse.Error.Exception.InternalServerError = true
75
+ c.JSON(http.StatusInternalServerError, apiResponse)
76
+ return
77
+ case errors.NotFoundError:
78
+ apiResponse.Message = specificErr.Message
79
+ apiResponse.Error.Details = specificErr.Error()
80
+ apiResponse.Error.Exception.DataNotFound = true
81
+ c.JSON(http.StatusNotFound, apiResponse)
82
+ return
83
+ case errors.UnauthorizedError:
84
+ apiResponse.Message = specificErr.Message
85
+ apiResponse.Error.Details = specificErr.Error()
86
+ apiResponse.Error.Exception.Unauthorized = true
87
+ c.JSON(http.StatusUnauthorized, apiResponse)
88
+ return
89
+ case errors.ForbiddenError:
90
+ apiResponse.Message = specificErr.Message
91
+ apiResponse.Error.Details = specificErr.Error()
92
+ apiResponse.Error.Exception.Forbidden = true
93
+ c.JSON(http.StatusForbidden, apiResponse)
94
+ return
95
+ case errors.BadRequestError:
96
+ apiResponse.Message = specificErr.Message
97
+ apiResponse.Error.Details = specificErr.Error()
98
+ apiResponse.Error.Exception.BadRequest = true
99
+ c.JSON(http.StatusBadRequest, apiResponse)
100
+ return
101
+ default:
102
+ apiResponse.Error.Details = "An unexpected error occurred."
103
+ apiResponse.Error.Exception.InternalServerError = true
104
+ c.JSON(http.StatusInternalServerError, apiResponse)
105
+ return
106
+ }
107
+ } else if validationErrors, ok := err.(validator.ValidationErrors); ok {
108
+ apiResponse.Error.Details = "Validation failed for the request."
109
+ apiResponse.Error.Exception.ValidationError = true
110
+ apiResponse.Error.ValidationErrors = make([]ValidationError, len(validationErrors))
111
+ for i, fe := range validationErrors {
112
+ apiResponse.Error.ValidationErrors[i] = ValidationError{
113
+ Field: fe.Field(),
114
+ Message: fe.Translate(nil), // adjust if you use translator
115
+ }
116
+ }
117
+ c.JSON(http.StatusBadRequest, apiResponse)
118
+ return
119
+ }
120
+
121
+ c.JSON(http.StatusInternalServerError, apiResponse)
122
+ apiResponse.Error.Details = err.Error()
123
+ }
124
+
125
+ func HandleSuccess(c *gin.Context, message string, statusCode int, data any, metaData any) {
126
+ apiResponse := API{
127
+ Status: "success",
128
+ Message: message,
129
+ Data: data,
130
+ MetaData: metaData,
131
+ }
132
+
133
+ if metaData == nil {
134
+ apiResponse.MetaData = struct{}{}
135
+ }
136
+
137
+ c.JSON(statusCode, apiResponse)
138
+ }
139
+
140
+ func HandleSuccessWithPaging(c *gin.Context, message string, content any, pagingInfo *PagingInfo) {
141
+
142
+ apiResponse := API{
143
+ Message: message,
144
+ Status: "success",
145
+ Data: List{
146
+ List: content,
147
+ },
148
+ MetaData: pagingInfo,
149
+ }
150
+
151
+ c.JSON(http.StatusOK, apiResponse)
152
+ }
space/space/space/router/health_check_route.go ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ package router
2
+
3
+ func (s *Server) HealthCheckRoute() {
4
+ s.router.GET("/api/v1/health-check", s.healthCheckController.Check)
5
+ }
space/space/space/router/router.go CHANGED
@@ -1,27 +1,20 @@
1
  package router
2
 
3
  import (
4
- "log"
5
-
6
- "api.qobiltu.id/config"
7
- "api.qobiltu.id/controller"
8
- "github.com/gin-gonic/gin"
9
  )
10
 
11
- func StartService() {
12
- router := gin.Default()
13
- router.Use(gin.Recovery())
14
 
15
- router.GET("/", controller.HomeController)
16
- AuthRoute(router)
17
- UserRoute(router)
18
- EmailRoute(router)
19
- OptionsRoute(router)
20
- AcademyRoute(router)
21
- QuizRoute(router)
22
 
23
- err := router.Run(config.TCP_ADDRESS)
24
- if err != nil {
25
- log.Fatalf("Failed to run server: %v", err)
26
- }
27
  }
 
1
  package router
2
 
3
  import (
4
+ "api.qobiltu.id/controller"
 
 
 
 
5
  )
6
 
7
+ func (s *Server) setupRoutes() {
8
+
9
+ s.router.GET("/", controller.HomeController)
10
 
11
+ AuthRoute(s.router)
12
+ UserRoute(s.router)
13
+ EmailRoute(s.router)
14
+ OptionsRoute(s.router)
15
+ AcademyRoute(s.router)
16
+ QuizRoute(s.router)
 
17
 
18
+ // another way to register routes
19
+ s.HealthCheckRoute()
 
 
20
  }
space/space/space/router/server.go ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package router
2
+
3
+ import (
4
+ "api.qobiltu.id/controller/health_check"
5
+ "github.com/gin-gonic/gin"
6
+ )
7
+
8
+ type Server struct {
9
+ router *gin.Engine
10
+ healthCheckController *health_check_controller.HealthCheckController
11
+ }
12
+
13
+ func NewServer(
14
+ healthCheckController *health_check_controller.HealthCheckController,
15
+ ) (*Server, error) {
16
+
17
+ router := gin.Default()
18
+ router.Use(gin.Recovery())
19
+
20
+ server := &Server{
21
+ healthCheckController: healthCheckController,
22
+ router: router,
23
+ }
24
+
25
+ server.setupRoutes()
26
+
27
+ return server, nil
28
+ }
29
+
30
+ func (s *Server) Start(address string) error {
31
+ return s.router.Run(address)
32
+ }
space/space/space/services/health_check_service/health_check_service.go ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package health_check_service
2
+
3
+ import (
4
+ "api.qobiltu.id/repositories"
5
+ )
6
+
7
+ type HealthCheckService struct {
8
+ healthCheckRepository *repositories.HealthCheckRepository
9
+ }
10
+
11
+ func NewHealthCheckService(healthCheckRepository *repositories.HealthCheckRepository) *HealthCheckService {
12
+ return &HealthCheckService{
13
+ healthCheckRepository: healthCheckRepository,
14
+ }
15
+ }
space/space/space/services/health_check_service/health_check_service_check.go ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package health_check_service
2
+
3
+ import (
4
+ "api.qobiltu.id/apperror"
5
+ "api.qobiltu.id/models"
6
+ "context"
7
+ )
8
+
9
+ func (s *HealthCheckService) Check(ctx context.Context, req *models.HealthCheckRequest) (*models.HealthCheckResponse, error) {
10
+
11
+ req.ExampleCallback = func() (string, string) {
12
+ return "OK", "OK"
13
+ }
14
+
15
+ res, err := s.healthCheckRepository.Check(ctx, req)
16
+ if err != nil {
17
+ return nil, apperror.NewInternalError("Failed to check health", err)
18
+ }
19
+
20
+ return res, nil
21
+ }
space/space/space/services/register_service.go CHANGED
@@ -1,86 +1,86 @@
1
  package services
2
 
3
  import (
4
- "errors"
5
- "unicode"
6
 
7
- "api.qobiltu.id/models"
8
- "api.qobiltu.id/repositories"
9
- uuid "github.com/satori/go.uuid"
10
- "gorm.io/gorm"
11
  )
12
 
13
  type RegisterService struct {
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
- )
24
 
25
- if len(password) >= 8 {
26
- hasMinLen = true
27
- }
28
 
29
- for _, char := range password {
30
- switch {
31
- case unicode.IsUpper(char):
32
- hasUpper = true
33
- break
34
- case unicode.IsLower(char):
35
- hasLower = true
36
- break
37
- case unicode.IsDigit(char):
38
- hasNumber = true
39
- break
40
- }
41
- }
42
- approve := hasMinLen && hasUpper && hasLower && hasNumber
43
- if !approve {
44
- return models.Exception{
45
- BadRequest: true,
46
- Message: "Password must contain at least 8 characters, including uppercase, lowercase, and number!",
47
- }
48
- }
49
- return models.Exception{}
50
  }
51
 
52
  func (s *RegisterService) Create() {
53
- validatePassword := ValidatePassword(s.Constructor.Password)
54
- if validatePassword.BadRequest {
55
- s.Exception = validatePassword
56
- return
57
- }
58
 
59
- hashedPassword, err := HashPassword(s.Constructor.Password)
60
- s.Error = err
61
- s.Constructor.Password = hashedPassword
62
- s.Constructor.UUID = uuid.NewV4()
63
- accountCreated := repositories.CreateAccount(s.Constructor)
64
 
65
- if errors.Is(accountCreated.RowsError, gorm.ErrDuplicatedKey) {
66
- s.Exception.DataDuplicate = true
67
- s.Exception.Message = "Account with email " + s.Constructor.Email + " already exists!"
68
- return
69
- } else if errors.Is(accountCreated.RowsError, gorm.ErrModelAccessibleFieldsRequired) || errors.Is(accountCreated.RowsError, gorm.ErrInvalidData) || errors.Is(accountCreated.RowsError, gorm.ErrInvalidValue) || errors.Is(accountCreated.RowsError, gorm.ErrInvalidField) {
70
- s.Exception.BadRequest = true
71
- s.Exception.Message = "Bad request!"
72
- return
73
- }
74
 
75
- userProfile := UserProfileService{}
76
- userProfile.Constructor.AccountID = accountCreated.Result.Id
77
- userProfile.Create()
78
- if userProfile.Error != nil {
79
- s.Error = userProfile.Error
80
- return
81
- }
82
 
83
- s.Error = accountCreated.RowsError
84
- s.Result = accountCreated.Result
85
- s.Result.Password = "SECRET"
86
  }
 
1
  package services
2
 
3
  import (
4
+ "errors"
5
+ "unicode"
6
 
7
+ "api.qobiltu.id/models"
8
+ "api.qobiltu.id/repositories"
9
+ uuid "github.com/satori/go.uuid"
10
+ "gorm.io/gorm"
11
  )
12
 
13
  type RegisterService struct {
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
+ )
24
 
25
+ if len(password) >= 8 {
26
+ hasMinLen = true
27
+ }
28
 
29
+ for _, char := range password {
30
+ switch {
31
+ case unicode.IsUpper(char):
32
+ hasUpper = true
33
+ break
34
+ case unicode.IsLower(char):
35
+ hasLower = true
36
+ break
37
+ case unicode.IsDigit(char):
38
+ hasNumber = true
39
+ break
40
+ }
41
+ }
42
+ approve := hasMinLen && hasUpper && hasLower && hasNumber
43
+ if !approve {
44
+ return models.Exception{
45
+ BadRequest: true,
46
+ Message: "Password must contain at least 8 characters, including uppercase, lowercase, and number!",
47
+ }
48
+ }
49
+ return models.Exception{}
50
  }
51
 
52
  func (s *RegisterService) Create() {
53
+ validatePassword := ValidatePassword(s.Constructor.Password)
54
+ if validatePassword.BadRequest {
55
+ s.Exception = validatePassword
56
+ return
57
+ }
58
 
59
+ hashedPassword, err := HashPassword(s.Constructor.Password)
60
+ s.Error = err
61
+ s.Constructor.Password = hashedPassword
62
+ s.Constructor.UUID = uuid.NewV4()
63
+ accountCreated := repositories.CreateAccount(s.Constructor)
64
 
65
+ if errors.Is(accountCreated.RowsError, gorm.ErrDuplicatedKey) {
66
+ s.Exception.DataDuplicate = true
67
+ s.Exception.Message = "Account with email " + s.Constructor.Email + " already exists!"
68
+ return
69
+ } else if errors.Is(accountCreated.RowsError, gorm.ErrModelAccessibleFieldsRequired) || errors.Is(accountCreated.RowsError, gorm.ErrInvalidData) || errors.Is(accountCreated.RowsError, gorm.ErrInvalidValue) || errors.Is(accountCreated.RowsError, gorm.ErrInvalidField) {
70
+ s.Exception.BadRequest = true
71
+ s.Exception.Message = "Bad request!"
72
+ return
73
+ }
74
 
75
+ userProfile := UserProfileService{}
76
+ userProfile.Constructor.AccountID = accountCreated.Result.Id
77
+ userProfile.Create()
78
+ if userProfile.Error != nil {
79
+ s.Error = userProfile.Error
80
+ return
81
+ }
82
 
83
+ s.Error = accountCreated.RowsError
84
+ s.Result = accountCreated.Result
85
+ s.Result.Password = "SECRET"
86
  }
space/space/space/space/.gitignore CHANGED
@@ -4,4 +4,5 @@ quzuu-be.exe
4
  README.md
5
  .qodo
6
  .error
7
- logs/
 
 
4
  README.md
5
  .qodo
6
  .error
7
+ logs/
8
+ .idea
space/space/space/space/Makefile ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ up-dev:
2
+ docker compose -f docker-compose.dev.yml up -d
3
+
4
+ run-dev:
5
+ go run main.go
6
+
7
+ .PHONY : up-dev run-dev
space/space/space/space/assets/efs.go ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package assets
2
+
3
+ import (
4
+ "embed"
5
+ )
6
+
7
+ //go:embed "emails"
8
+ var EmbeddedFiles embed.FS
9
+
10
+ const (
11
+ EmailConfirmationTemplatePath = "emails/email-confirmation.tmpl"
12
+ EmailForgotPasswordTemplatePath = "emails/email-forgot-password.tmpl"
13
+ )
space/space/space/space/assets/emails/email-confirmation.tmpl ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {{define "htmlBody"}}
2
+ <!DOCTYPE html>
3
+ <html>
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <style>
7
+ body {
8
+ font-family: Arial, sans-serif;
9
+ background-color: #f4f4f4;
10
+ margin: 0;
11
+ padding: 0;
12
+ }
13
+ .container {
14
+ background-color: #ffffff;
15
+ max-width: 600px;
16
+ margin: 40px auto;
17
+ padding: 30px;
18
+ border-radius: 8px;
19
+ box-shadow: 0 4px 12px rgba(0,0,0,0.1);
20
+ color: #333;
21
+ }
22
+ .code-box {
23
+ font-size: 28px;
24
+ font-weight: bold;
25
+ letter-spacing: 8px;
26
+ background: #f0f8ff;
27
+ border: 2px dashed #007BFF;
28
+ padding: 15px 25px;
29
+ margin: 30px 0;
30
+ text-align: center;
31
+ border-radius: 6px;
32
+ color: #007BFF;
33
+ }
34
+ .footer {
35
+ font-size: 12px;
36
+ color: #aaa;
37
+ margin-top: 20px;
38
+ text-align: center;
39
+ }
40
+ </style>
41
+ </head>
42
+ <body>
43
+ <div class="container">
44
+ <p>Berikut adalah kode verifikasi Anda:</p>
45
+ <div class="code-box">{{.VerificationCode}}</div>
46
+ <p>Silakan masukkan kode ini untuk memverifikasi alamat email Anda. Kode ini akan kedaluwarsa dalam <strong>{{.ExpirationInMinutes}} menit</strong>.</p>
47
+ <p>Jika Anda tidak meminta ini, abaikan email ini.</p>
48
+ <p>Salam hormat,<br><strong>Tim Support Qobiltu Indonesia</strong></p>
49
+ <div class="footer">
50
+ Email ini dikirim otomatis oleh sistem. Jangan membalas email ini.
51
+ </div>
52
+ </div>
53
+ </body>
54
+ </html>
55
+ {{end}}
space/space/space/space/assets/emails/email-forgot-password.tmpl ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {{define "htmlBody"}}
2
+ <!DOCTYPE html>
3
+ <html>
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <style>
7
+ body {
8
+ font-family: Arial, sans-serif;
9
+ background-color: #f4f4f4;
10
+ margin: 0;
11
+ padding: 0;
12
+ }
13
+ .container {
14
+ background-color: #ffffff;
15
+ max-width: 600px;
16
+ margin: 40px auto;
17
+ padding: 30px;
18
+ border-radius: 8px;
19
+ box-shadow: 0 4px 12px rgba(0,0,0,0.1);
20
+ color: #333;
21
+ }
22
+ .code-box {
23
+ font-size: 28px;
24
+ font-weight: bold;
25
+ letter-spacing: 8px;
26
+ background: #f0f8ff;
27
+ border: 2px dashed #007BFF;
28
+ padding: 15px 25px;
29
+ margin: 30px 0;
30
+ text-align: center;
31
+ border-radius: 6px;
32
+ color: #007BFF;
33
+ }
34
+ .footer {
35
+ font-size: 12px;
36
+ color: #aaa;
37
+ margin-top: 20px;
38
+ text-align: center;
39
+ }
40
+ </style>
41
+ </head>
42
+ <body>
43
+ <div class="container">
44
+ <p>Anda menerima email ini karena ada permintaan untuk mengatur ulang kata sandi akun Anda.</p>
45
+ <p>Berikut adalah kode verifikasi Anda:</p>
46
+ <div class="code-box">{{.ResetToken}}</div>
47
+ <p>Tautan ini akan kedaluwarsa dalam <strong>{{.ExpirationInMinutes}} menit</strong>.</p>
48
+ <p>Jika Anda tidak meminta pengaturan ulang kata sandi ini, abaikan email ini. Tidak ada perubahan yang akan dilakukan pada akun Anda.</p>
49
+ <p>Salam hormat,<br><strong>Tim Support Qobiltu Indonesia</strong></p>
50
+ <div class="footer">
51
+ Email ini dikirim otomatis oleh sistem. Jangan membalas email ini.
52
+ </div>
53
+ </div>
54
+ </body>
55
+ </html>
56
+ {{end}}
space/space/space/space/config/config.go CHANGED
@@ -1,36 +1,63 @@
1
- package config
2
-
3
- import (
4
- "os"
5
- "strconv"
6
-
7
- "github.com/joho/godotenv"
8
- )
9
-
10
- var ENV string
11
- var TCP_ADDRESS string
12
- var LOG_PATH string
13
-
14
- var HOST_ADDRESS string
15
- var HOST_PORT string
16
- var EMAIL_VERIFICATION_DURATION int
17
-
18
- var SMTP_SENDER_EMAIL string
19
- var SMTP_SENDER_PASSWORD string
20
- var SMTP_HOST string
21
- var SMTP_PORT string
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
29
- LOG_PATH = os.Getenv("LOG_PATH")
30
- EMAIL_VERIFICATION_DURATION, _ = strconv.Atoi(os.Getenv("EMAIL_VERIFICATION_DURATION"))
31
- SMTP_SENDER_EMAIL = os.Getenv("SMTP_SENDER_EMAIL")
32
- SMTP_SENDER_PASSWORD = os.Getenv("SMTP_SENDER_PASSWORD")
33
- SMTP_HOST = os.Getenv("SMTP_HOST")
34
- SMTP_PORT = os.Getenv("SMTP_PORT")
35
- // Menampilkan nilai variabel lingkungan
36
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package config
2
+
3
+ import (
4
+ "os"
5
+ "strconv"
6
+ "time"
7
+
8
+ "github.com/joho/godotenv"
9
+ )
10
+
11
+ var ENV string
12
+ var TCP_ADDRESS string
13
+ var LOG_PATH string
14
+
15
+ var HOST_ADDRESS string
16
+ var HOST_PORT string
17
+ var EMAIL_VERIFICATION_DURATION int
18
+
19
+ var SMTP_SENDER_EMAIL string
20
+ var SMTP_SENDER_PASSWORD string
21
+ var SMTP_HOST string
22
+ var SMTP_PORT string
23
+
24
+ var REDIS_HOST string
25
+ var REDIS_PORT int
26
+ var REDIS_PASSWORD string
27
+ var REDIS_DB int
28
+ var REDIS_MIN_IDLE_CONNS int
29
+ var REDIS_POOL_SIZE int
30
+ var REDIS_POOL_TIMEOUT time.Duration
31
+
32
+ func init() {
33
+ godotenv.Load()
34
+ ENV = os.Getenv("ENV")
35
+ HOST_ADDRESS = os.Getenv("HOST_ADDRESS")
36
+ HOST_PORT = os.Getenv("HOST_PORT")
37
+ TCP_ADDRESS = HOST_ADDRESS + ":" + HOST_PORT
38
+ LOG_PATH = os.Getenv("LOG_PATH")
39
+ EMAIL_VERIFICATION_DURATION, _ = strconv.Atoi(os.Getenv("EMAIL_VERIFICATION_DURATION"))
40
+ SMTP_SENDER_EMAIL = os.Getenv("SMTP_SENDER_EMAIL")
41
+ SMTP_SENDER_PASSWORD = os.Getenv("SMTP_SENDER_PASSWORD")
42
+ SMTP_HOST = os.Getenv("SMTP_HOST")
43
+ SMTP_PORT = os.Getenv("SMTP_PORT")
44
+
45
+ REDIS_HOST = getValue(os.Getenv("REDIS_HOST"), "redis", func(s string) (string, error) { return s, nil })
46
+ REDIS_PORT = getValue(os.Getenv("REDIS_PORT"), 6379, func(s string) (int, error) { return strconv.Atoi(s) })
47
+ REDIS_PASSWORD = getValue(os.Getenv("REDIS_PASSWORD"), "qobiltu", func(s string) (string, error) { return s, nil })
48
+ REDIS_DB = getValue(os.Getenv("REDIS_DB"), 0, func(s string) (int, error) { return strconv.Atoi(s) })
49
+ REDIS_POOL_SIZE = getValue(os.Getenv("REDIS_POOL_SIZE"), 10, func(s string) (int, error) { return strconv.Atoi(s) })
50
+ REDIS_MIN_IDLE_CONNS = getValue(os.Getenv("REDIS_MIN_IDLE_CONNS"), 10, func(s string) (int, error) { return strconv.Atoi(s) })
51
+ REDIS_POOL_TIMEOUT = getValue(os.Getenv("REDIS_POOL_TIMEOUT"), time.Second*30, func(s string) (time.Duration, error) { return time.ParseDuration(s) })
52
+ }
53
+
54
+ func getValue[T any](value string, defaultValue T, convert func(string) (T, error)) T {
55
+ if value == "" {
56
+ return defaultValue
57
+ }
58
+ convertedValue, err := convert(value)
59
+ if err != nil {
60
+ return defaultValue
61
+ }
62
+ return convertedValue
63
+ }
space/space/space/space/controller/api_response.go ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package controller
2
+
3
+ import (
4
+ "net/http"
5
+ "reflect"
6
+
7
+ "api.qobiltu.id/models"
8
+ "api.qobiltu.id/services"
9
+
10
+ "github.com/gin-gonic/gin"
11
+ )
12
+
13
+ func ResponseOK(c *gin.Context, data any) {
14
+ res := models.SuccessResponse{
15
+ Status: "success",
16
+ Message: "Data retrieved successfully!",
17
+ Data: data,
18
+ MetaData: c.Request.Body,
19
+ }
20
+ c.JSON(http.StatusOK, res)
21
+ return
22
+ }
23
+
24
+ func ResponseFAIL(c *gin.Context, status int, exception models.Exception) {
25
+ message := exception.Message
26
+ exception.Message = ""
27
+ res := models.ErrorResponse{
28
+ Status: "error",
29
+ Message: message,
30
+ Errors: exception,
31
+ MetaData: c.Request.Body,
32
+ }
33
+ c.AbortWithStatusJSON(status, res)
34
+ return
35
+ }
36
+
37
+ func SendResponse(c *gin.Context, data services.Service[any, any]) {
38
+ if reflect.ValueOf(data.Exception).IsNil() {
39
+ ResponseOK(c, data)
40
+ } else {
41
+ if data.Exception.Unauthorized {
42
+ ResponseFAIL(c, 401, data.Exception)
43
+ } else if data.Exception.BadRequest {
44
+ ResponseFAIL(c, 400, data.Exception)
45
+ } else if data.Exception.DataNotFound {
46
+ ResponseFAIL(c, 404, data.Exception)
47
+ } else if data.Exception.InternalServerError {
48
+ ResponseFAIL(c, 500, data.Exception)
49
+ } else {
50
+ ResponseFAIL(c, 403, data.Exception)
51
+ }
52
+ }
53
+ }
space/space/space/space/controller/controller.go CHANGED
@@ -1,65 +1,65 @@
1
- package controller
2
-
3
- import (
4
- "api.qobiltu.id/models"
5
- "api.qobiltu.id/services"
6
- "api.qobiltu.id/utils"
7
- "github.com/gin-gonic/gin"
8
- )
9
-
10
- type (
11
- Controllers interface {
12
- RequestJSON(c *gin.Context)
13
- Response(c *gin.Context)
14
- }
15
- Controller[T1 any, T2 any, T3 any] struct {
16
- AccountData models.AccountData
17
- Request T1
18
- Service *services.Service[T2, T3]
19
- }
20
- )
21
-
22
- func (controller *Controller[T1, T2, T3]) HeaderParse(c *gin.Context, act func()) {
23
- cParam, _ := c.Get("accountData")
24
- if cParam != nil {
25
- controller.AccountData = cParam.(models.AccountData)
26
- }
27
- act()
28
- }
29
- func (controller *Controller[T1, T2, T3]) RequestJSON(c *gin.Context, act func()) {
30
- cParam, _ := c.Get("accountData")
31
- if cParam != nil {
32
- controller.AccountData = cParam.(models.AccountData)
33
- }
34
- errBinding := c.ShouldBindJSON(&controller.Request)
35
- if errBinding != nil {
36
- utils.ResponseFAIL(c, 400, models.Exception{
37
- BadRequest: true,
38
- Message: "Invalid Request!, recheck your request, there's must be some problem about required parameter or type parameter",
39
- })
40
- return
41
- } else {
42
- act()
43
- controller.Response(c)
44
- }
45
- }
46
- func (controller *Controller[T1, T2, T3]) Response(c *gin.Context) {
47
- switch {
48
- case controller.Service.Error != nil:
49
- utils.ResponseFAIL(c, 500, models.Exception{
50
- InternalServerError: true,
51
- Message: "Internal Server Error",
52
- })
53
- utils.LogError(controller.Service.Error)
54
- case controller.Service.Exception.DataDuplicate:
55
- utils.ResponseFAIL(c, 400, controller.Service.Exception)
56
- case controller.Service.Exception.Unauthorized:
57
- utils.ResponseFAIL(c, 401, controller.Service.Exception)
58
- case controller.Service.Exception.DataNotFound:
59
- utils.ResponseFAIL(c, 404, controller.Service.Exception)
60
- case controller.Service.Exception.Message != "":
61
- utils.ResponseFAIL(c, 400, controller.Service.Exception)
62
- default:
63
- utils.ResponseOK(c, controller.Service.Result)
64
- }
65
- }
 
1
+ package controller
2
+
3
+ import (
4
+ "api.qobiltu.id/models"
5
+ "api.qobiltu.id/services"
6
+ "api.qobiltu.id/utils"
7
+ "github.com/gin-gonic/gin"
8
+ )
9
+
10
+ type (
11
+ Controllers interface {
12
+ RequestJSON(c *gin.Context)
13
+ Response(c *gin.Context)
14
+ }
15
+ Controller[T1 any, T2 any, T3 any] struct {
16
+ AccountData models.AccountData
17
+ Request T1
18
+ Service *services.Service[T2, T3]
19
+ }
20
+ )
21
+
22
+ func (controller *Controller[T1, T2, T3]) HeaderParse(c *gin.Context, act func()) {
23
+ cParam, _ := c.Get("accountData")
24
+ if cParam != nil {
25
+ controller.AccountData = cParam.(models.AccountData)
26
+ }
27
+ act()
28
+ }
29
+ func (controller *Controller[T1, T2, T3]) RequestJSON(c *gin.Context, act func()) {
30
+ cParam, _ := c.Get("accountData")
31
+ if cParam != nil {
32
+ controller.AccountData = cParam.(models.AccountData)
33
+ }
34
+ errBinding := c.ShouldBindJSON(&controller.Request)
35
+ if errBinding != nil {
36
+ ResponseFAIL(c, 400, models.Exception{
37
+ BadRequest: true,
38
+ Message: "Invalid Request!, recheck your request, there's must be some problem about required parameter or type parameter",
39
+ })
40
+ return
41
+ } else {
42
+ act()
43
+ controller.Response(c)
44
+ }
45
+ }
46
+ func (controller *Controller[T1, T2, T3]) Response(c *gin.Context) {
47
+ switch {
48
+ case controller.Service.Error != nil:
49
+ utils.LogError(controller.Service.Error)
50
+ ResponseFAIL(c, 500, models.Exception{
51
+ InternalServerError: true,
52
+ Message: "Internal Server Error",
53
+ })
54
+ case controller.Service.Exception.DataDuplicate:
55
+ ResponseFAIL(c, 400, controller.Service.Exception)
56
+ case controller.Service.Exception.Unauthorized:
57
+ ResponseFAIL(c, 401, controller.Service.Exception)
58
+ case controller.Service.Exception.DataNotFound:
59
+ ResponseFAIL(c, 404, controller.Service.Exception)
60
+ case controller.Service.Exception.Message != "":
61
+ ResponseFAIL(c, 400, controller.Service.Exception)
62
+ default:
63
+ ResponseOK(c, controller.Service.Result)
64
+ }
65
+ }
space/space/space/space/controller/email/email_create_verification_controller.go CHANGED
@@ -1,21 +1,21 @@
1
- package controller
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 CreateVerification(c *gin.Context) {
11
- emailVerification := services.EmailVerificationService{}
12
- emailVerificationController := controller.Controller[models.CreateVerifyEmailRequest, models.EmailVerification, models.EmailVerification]{
13
- Service: &emailVerification.Service,
14
- }
15
- emailVerificationController.HeaderParse(c, func() {
16
- emailVerificationController.Service.Constructor.AccountID = uint(emailVerificationController.AccountData.UserID)
17
- emailVerification.Create()
18
- emailVerificationController.Response(c)
19
- })
20
-
21
- }
 
1
+ package controller
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 CreateVerification(c *gin.Context) {
11
+ emailVerification := services.EmailVerificationService{}
12
+ emailVerificationController := controller.Controller[models.CreateVerifyEmailRequest, models.EmailVerification, models.EmailVerification]{
13
+ Service: &emailVerification.Service,
14
+ }
15
+ emailVerificationController.HeaderParse(c, func() {
16
+ emailVerificationController.Service.Constructor.AccountID = emailVerificationController.AccountData.UserID
17
+ emailVerification.Create()
18
+ emailVerificationController.Response(c)
19
+ })
20
+
21
+ }
space/space/space/space/docker-compose.dev.yml ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3.8'
2
+
3
+ services:
4
+ db:
5
+ image: postgres:15
6
+ container_name: postgres-db
7
+ environment:
8
+ POSTGRES_USER: ${DB_USER:-qobiltu}
9
+ POSTGRES_PASSWORD: ${DB_PASSWORD:-qobiltu}
10
+ POSTGRES_DB: ${DB_NAME:-qobiltu}
11
+ ports:
12
+ - "5432:5432"
13
+ volumes:
14
+ - db-data:/var/lib/postgresql/data
15
+ restart: always
16
+
17
+ volumes:
18
+ db-data:
space/space/space/space/docker-compose.yml CHANGED
@@ -9,6 +9,8 @@ services:
9
  env_file: .env
10
  ports:
11
  - "8080:8080"
 
 
12
  # volumes:
13
  # - ./logs:/app/logs
14
  # - /home/qobiltu/api-qobiltu:/app
@@ -25,7 +27,28 @@ services:
25
  - "5432:5432"
26
  volumes:
27
  - /home/qobiltu/postgres-data:/var/lib/postgresql/data
 
 
28
  restart: always
29
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  volumes:
31
  db-data:
 
 
 
 
 
 
9
  env_file: .env
10
  ports:
11
  - "8080:8080"
12
+ networks:
13
+ - qobiltu-network
14
  # volumes:
15
  # - ./logs:/app/logs
16
  # - /home/qobiltu/api-qobiltu:/app
 
27
  - "5432:5432"
28
  volumes:
29
  - /home/qobiltu/postgres-data:/var/lib/postgresql/data
30
+ networks:
31
+ - qobiltu-network
32
  restart: always
33
 
34
+ redis:
35
+ image: redis/redis-stack:7.2.0-v11
36
+ container_name: redis-db
37
+ environment:
38
+ REDIS_ARGS: "--requirepass ${REDIS_PASSWORD:-qobiltu}"
39
+ ports:
40
+ - "8001:8001"
41
+ - "6379:6379"
42
+ volumes:
43
+ - /home/qobiltu/redis-data:/data
44
+ networks:
45
+ - qobiltu-network
46
+ restart: always
47
+
48
  volumes:
49
  db-data:
50
+ redis-data:
51
+
52
+ networks:
53
+ qobiltu-network:
54
+ driver: bridge
space/space/space/space/go.mod CHANGED
@@ -6,7 +6,10 @@ require (
6
  github.com/gin-gonic/gin v1.10.0
7
  github.com/golang-jwt/jwt/v5 v5.2.1
8
  github.com/gosimple/slug v1.15.0
 
9
  github.com/joho/godotenv v1.5.1
 
 
10
  github.com/satori/go.uuid v1.2.0
11
  golang.org/x/crypto v0.36.0
12
  google.golang.org/api v0.228.0
@@ -20,7 +23,9 @@ require (
20
  cloud.google.com/go/compute/metadata v0.6.0 // indirect
21
  github.com/bytedance/sonic v1.13.1 // indirect
22
  github.com/bytedance/sonic/loader v0.2.4 // indirect
 
23
  github.com/cloudwego/base64x v0.1.5 // indirect
 
24
  github.com/felixge/httpsnoop v1.0.4 // indirect
25
  github.com/gabriel-vasile/mimetype v1.4.8 // indirect
26
  github.com/gin-contrib/sse v1.0.0 // indirect
@@ -31,6 +36,7 @@ require (
31
  github.com/go-playground/validator/v10 v10.25.0 // indirect
32
  github.com/goccy/go-json v0.10.5 // indirect
33
  github.com/google/s2a-go v0.1.9 // indirect
 
34
  github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
35
  github.com/googleapis/gax-go/v2 v2.14.1 // indirect
36
  github.com/gosimple/unidecode v1.0.1 // indirect
@@ -47,6 +53,8 @@ require (
47
  github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
48
  github.com/modern-go/reflect2 v1.0.2 // indirect
49
  github.com/pelletier/go-toml/v2 v2.2.3 // indirect
 
 
50
  github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
51
  github.com/ugorji/go/codec v1.2.12 // indirect
52
  go.opentelemetry.io/auto/sdk v1.1.0 // indirect
@@ -60,6 +68,7 @@ require (
60
  golang.org/x/sync v0.12.0 // indirect
61
  golang.org/x/sys v0.31.0 // indirect
62
  golang.org/x/text v0.23.0 // indirect
 
63
  google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 // indirect
64
  google.golang.org/grpc v1.71.0 // indirect
65
  google.golang.org/protobuf v1.36.6 // indirect
 
6
  github.com/gin-gonic/gin v1.10.0
7
  github.com/golang-jwt/jwt/v5 v5.2.1
8
  github.com/gosimple/slug v1.15.0
9
+ github.com/hibiken/asynq v0.25.1
10
  github.com/joho/godotenv v1.5.1
11
+ github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible
12
+ github.com/redis/go-redis/v9 v9.7.0
13
  github.com/satori/go.uuid v1.2.0
14
  golang.org/x/crypto v0.36.0
15
  google.golang.org/api v0.228.0
 
23
  cloud.google.com/go/compute/metadata v0.6.0 // indirect
24
  github.com/bytedance/sonic v1.13.1 // indirect
25
  github.com/bytedance/sonic/loader v0.2.4 // indirect
26
+ github.com/cespare/xxhash/v2 v2.3.0 // indirect
27
  github.com/cloudwego/base64x v0.1.5 // indirect
28
+ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
29
  github.com/felixge/httpsnoop v1.0.4 // indirect
30
  github.com/gabriel-vasile/mimetype v1.4.8 // indirect
31
  github.com/gin-contrib/sse v1.0.0 // indirect
 
36
  github.com/go-playground/validator/v10 v10.25.0 // indirect
37
  github.com/goccy/go-json v0.10.5 // indirect
38
  github.com/google/s2a-go v0.1.9 // indirect
39
+ github.com/google/uuid v1.6.0 // indirect
40
  github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
41
  github.com/googleapis/gax-go/v2 v2.14.1 // indirect
42
  github.com/gosimple/unidecode v1.0.1 // indirect
 
53
  github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
54
  github.com/modern-go/reflect2 v1.0.2 // indirect
55
  github.com/pelletier/go-toml/v2 v2.2.3 // indirect
56
+ github.com/robfig/cron/v3 v3.0.1 // indirect
57
+ github.com/spf13/cast v1.7.0 // indirect
58
  github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
59
  github.com/ugorji/go/codec v1.2.12 // indirect
60
  go.opentelemetry.io/auto/sdk v1.1.0 // indirect
 
68
  golang.org/x/sync v0.12.0 // indirect
69
  golang.org/x/sys v0.31.0 // indirect
70
  golang.org/x/text v0.23.0 // indirect
71
+ golang.org/x/time v0.11.0 // indirect
72
  google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 // indirect
73
  google.golang.org/grpc v1.71.0 // indirect
74
  google.golang.org/protobuf v1.36.6 // indirect
space/space/space/space/go.sum CHANGED
@@ -4,19 +4,29 @@ cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIi
4
  cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
5
  cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
6
  cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
 
 
 
 
7
  github.com/bytedance/sonic v1.13.1 h1:Jyd5CIvdFnkOWuKXr+wm4Nyk2h0yAFsr8ucJgEasO3g=
8
  github.com/bytedance/sonic v1.13.1/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
9
  github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
10
  github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
11
  github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
 
 
12
  github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
13
  github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
14
  github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
15
  github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
16
  github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
17
  github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 
 
18
  github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
19
  github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
 
 
20
  github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
21
  github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
22
  github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E=
@@ -57,6 +67,8 @@ github.com/gosimple/slug v1.15.0 h1:wRZHsRrRcs6b0XnxMUBM6WK1U1Vg5B0R7VkIf1Xzobo=
57
  github.com/gosimple/slug v1.15.0/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ=
58
  github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o=
59
  github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc=
 
 
60
  github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
61
  github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
62
  github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
@@ -71,6 +83,8 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
71
  github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
72
  github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
73
  github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
 
 
74
  github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
75
  github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
76
  github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
@@ -94,10 +108,16 @@ github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNH
94
  github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
95
  github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
96
  github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 
 
 
 
97
  github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
98
  github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
99
  github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
100
  github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
 
 
101
  github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
102
  github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
103
  github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@@ -130,6 +150,8 @@ go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce
130
  go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
131
  go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
132
  go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
 
 
133
  golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw=
134
  golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
135
  golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
 
4
  cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
5
  cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
6
  cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
7
+ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
8
+ github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
9
+ github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
10
+ github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
11
  github.com/bytedance/sonic v1.13.1 h1:Jyd5CIvdFnkOWuKXr+wm4Nyk2h0yAFsr8ucJgEasO3g=
12
  github.com/bytedance/sonic v1.13.1/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
13
  github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
14
  github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
15
  github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
16
+ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
17
+ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
18
  github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
19
  github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
20
  github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
21
  github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
22
  github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
23
  github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
24
+ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
25
+ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
26
  github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
27
  github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
28
+ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
29
+ github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
30
  github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
31
  github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
32
  github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E=
 
67
  github.com/gosimple/slug v1.15.0/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ=
68
  github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o=
69
  github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc=
70
+ github.com/hibiken/asynq v0.25.1 h1:phj028N0nm15n8O2ims+IvJ2gz4k2auvermngh9JhTw=
71
+ github.com/hibiken/asynq v0.25.1/go.mod h1:pazWNOLBu0FEynQRBvHA26qdIKRSmfdIfUm4HdsLmXg=
72
  github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
73
  github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
74
  github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
 
83
  github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
84
  github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
85
  github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
86
+ github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA=
87
+ github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A=
88
  github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
89
  github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
90
  github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
 
108
  github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
109
  github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
110
  github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
111
+ github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E=
112
+ github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
113
+ github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
114
+ github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
115
  github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
116
  github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
117
  github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
118
  github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
119
+ github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
120
+ github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
121
  github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
122
  github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
123
  github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
 
150
  go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
151
  go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
152
  go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
153
+ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
154
+ go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
155
  golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw=
156
  golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
157
  golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
space/space/space/space/mail/sender.go ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ package mail
2
+
3
+ type Sender interface {
4
+ Send(recipient, subject string, htmlContent string, data any) error
5
+ }
6
+
7
+ // EmailSender is a global variable to hold the email sender
8
+ var EmailSender Sender
space/space/space/space/mail/smtp.go ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package mail
2
+
3
+ import (
4
+ "errors"
5
+ "fmt"
6
+ "github.com/jordan-wright/email"
7
+ "net/mail"
8
+ "net/smtp"
9
+ )
10
+
11
+ var (
12
+ ErrEmailEmpty = errors.New("mail is empty")
13
+ ErrEmailInvalid = errors.New("invalid email")
14
+ )
15
+
16
+ // Config menyimpan pengaturan untuk koneksi SMTP.
17
+ type Config struct {
18
+ Host string
19
+ Port string
20
+ Username string
21
+ Password string
22
+ From string
23
+ }
24
+
25
+ // SMTP adalah implementasi Sender untuk mengirim mail melalui SMTP
26
+ type SMTP struct {
27
+ name string
28
+ fromEmailAddress string
29
+ smtpServerAddress string
30
+ smtpAuthAddress smtp.Auth
31
+ }
32
+
33
+ // New membuat instance baru SMTP dengan konfigurasi.
34
+ func New(cfg *Config) (Sender, error) {
35
+ if err := validateEmail(cfg.From); err != nil {
36
+ return nil, fmt.Errorf("invalid from address: %w", err)
37
+ }
38
+
39
+ return &SMTP{
40
+ name: cfg.From,
41
+ fromEmailAddress: cfg.From,
42
+ smtpServerAddress: fmt.Sprintf("%s:%s", cfg.Host, cfg.Port),
43
+ smtpAuthAddress: smtp.PlainAuth("", cfg.Username, cfg.Password, cfg.Host),
44
+ }, nil
45
+ }
46
+
47
+ // Send mengirim mail ke penerima dengan subjek, konten HTML, dan data lainnya.
48
+ func (s *SMTP) Send(recipient, subject string, htmlContent string, data any) error {
49
+ e := email.NewEmail()
50
+ e.From = s.fromEmailAddress
51
+ e.To = []string{recipient}
52
+ e.Subject = subject
53
+ e.HTML = []byte(htmlContent)
54
+ return s.sendEmail(e)
55
+ }
56
+
57
+ func (s *SMTP) sendEmail(e *email.Email) error {
58
+ return e.Send(s.smtpServerAddress, s.smtpAuthAddress)
59
+ }
60
+
61
+ func validateEmail(email string) error {
62
+ if email == "" {
63
+ return ErrEmailEmpty
64
+ }
65
+
66
+ if _, err := mail.ParseAddress(email); err != nil {
67
+ return ErrEmailInvalid
68
+ }
69
+
70
+ return nil
71
+ }
space/space/space/space/main.go CHANGED
@@ -1,14 +1,62 @@
1
- package main
2
-
3
- import (
4
- "fmt"
5
-
6
- "api.qobiltu.id/config"
7
- "api.qobiltu.id/router"
8
- )
9
-
10
- func main() {
11
- fmt.Println("Server started on ", config.TCP_ADDRESS, ", port :", config.HOST_PORT)
12
- router.StartService()
13
-
14
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package main
2
+
3
+ import (
4
+ "api.qobiltu.id/config"
5
+ "api.qobiltu.id/controller/health_check"
6
+ "api.qobiltu.id/mail"
7
+ "api.qobiltu.id/repositories"
8
+ "api.qobiltu.id/router"
9
+ "api.qobiltu.id/services/health_check_service"
10
+ "api.qobiltu.id/utils"
11
+ "api.qobiltu.id/worker"
12
+ "github.com/hibiken/asynq"
13
+ "log/slog"
14
+ "net"
15
+ "strconv"
16
+ )
17
+
18
+ func main() {
19
+
20
+ // setup email sender
21
+ emailConfig := mail.Config{
22
+ Host: config.SMTP_HOST,
23
+ Port: config.SMTP_PORT,
24
+ From: config.SMTP_SENDER_EMAIL,
25
+ Username: config.SMTP_SENDER_EMAIL,
26
+ Password: config.SMTP_SENDER_PASSWORD,
27
+ }
28
+
29
+ emailSender, err := mail.New(&emailConfig)
30
+ utils.FatalIfErr("failed to setup email sender", err)
31
+ mail.EmailSender = emailSender
32
+
33
+ // setup redis task distributor and processor
34
+ asynqRedisOpt := asynq.RedisClientOpt{
35
+ Addr: net.JoinHostPort(config.REDIS_HOST, strconv.Itoa(config.REDIS_PORT)),
36
+ Password: config.REDIS_PASSWORD,
37
+ DB: config.REDIS_DB,
38
+ }
39
+
40
+ taskDistributor := worker.NewRedisTaskDistributor(asynqRedisOpt)
41
+ taskProcessor := worker.NewRedisTaskProcessor(asynqRedisOpt, emailSender)
42
+ worker.AsyncTaskDistributor = taskDistributor
43
+
44
+ // setup repo, service, and controller
45
+ healthCheckRepository := repositories.NewHealthCheckRepository(config.DB)
46
+ healthCheckService := health_check_service.NewHealthCheckService(healthCheckRepository)
47
+ healthCheckController := health_check_controller.NewHealthCheckController(healthCheckService)
48
+
49
+ // start task processor
50
+ err = taskProcessor.Start()
51
+ utils.FatalIfErr("failed to start task processor", err)
52
+ slog.Info("Task processor started")
53
+
54
+ // create server
55
+ s, err := router.NewServer(healthCheckController)
56
+ utils.FatalIfErr("failed to create server", err)
57
+
58
+ // run server
59
+ slog.Info("Starting server", "address", config.TCP_ADDRESS, "port", config.HOST_PORT)
60
+ err = s.Start(config.TCP_ADDRESS)
61
+ utils.FatalIfErr("failed to start server", err)
62
+ }
space/space/space/space/middleware/authentication_middleware.go CHANGED
@@ -1,37 +1,37 @@
1
- // auth/auth.go
2
-
3
- package middleware
4
-
5
- import (
6
- "api.qobiltu.id/models"
7
- "api.qobiltu.id/services"
8
- "api.qobiltu.id/utils"
9
- "github.com/gin-gonic/gin"
10
- )
11
-
12
- func AuthUser(c *gin.Context) {
13
- var currAccData models.AccountData
14
- if c.Request.Header["Authorization"] != nil {
15
- token := c.Request.Header["Authorization"]
16
-
17
- currAccData.UserID, currAccData.VerifyStatus, currAccData.ErrVerif = services.VerifyToken(token[0])
18
-
19
- if currAccData.VerifyStatus == "invalid-token" || currAccData.VerifyStatus == "expired" {
20
- currAccData.UserID = 0
21
- utils.ResponseFAIL(c, 401, models.Exception{Unauthorized: true, Message: "Your session is expired, Please re-Login!"})
22
- c.Abort()
23
- return
24
- } else {
25
- c.Set("accountData", currAccData)
26
- c.Next()
27
- }
28
- } else {
29
- currAccData.UserID = 0
30
- currAccData.VerifyStatus = "no-token"
31
- currAccData.ErrVerif = nil
32
- utils.ResponseFAIL(c, 401, models.Exception{Unauthorized: true, Message: "You have to login first!"})
33
- c.Abort()
34
- return
35
- }
36
-
37
- }
 
1
+ // auth/auth.go
2
+
3
+ package middleware
4
+
5
+ import (
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 AuthUser(c *gin.Context) {
13
+ var currAccData models.AccountData
14
+ if c.Request.Header["Authorization"] != nil {
15
+ token := c.Request.Header["Authorization"]
16
+
17
+ currAccData.UserID, currAccData.VerifyStatus, currAccData.ErrVerif = services.VerifyToken(token[0])
18
+
19
+ if currAccData.VerifyStatus == "invalid-token" || currAccData.VerifyStatus == "expired" {
20
+ currAccData.UserID = 0
21
+ controller.ResponseFAIL(c, 401, models.Exception{Unauthorized: true, Message: "Your session is expired, Please re-Login!"})
22
+ c.Abort()
23
+ return
24
+ } else {
25
+ c.Set("accountData", currAccData)
26
+ c.Next()
27
+ }
28
+ } else {
29
+ currAccData.UserID = 0
30
+ currAccData.VerifyStatus = "no-token"
31
+ currAccData.ErrVerif = nil
32
+ controller.ResponseFAIL(c, 401, models.Exception{Unauthorized: true, Message: "You have to login first!"})
33
+ c.Abort()
34
+ return
35
+ }
36
+
37
+ }
space/space/space/space/router/router.go CHANGED
@@ -1,24 +1,27 @@
1
- package router
2
-
3
- import (
4
- "log"
5
-
6
- "api.qobiltu.id/config"
7
- "api.qobiltu.id/controller"
8
- "github.com/gin-gonic/gin"
9
- )
10
-
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)
23
- }
24
- }
 
 
 
 
1
+ package router
2
+
3
+ import (
4
+ "log"
5
+
6
+ "api.qobiltu.id/config"
7
+ "api.qobiltu.id/controller"
8
+ "github.com/gin-gonic/gin"
9
+ )
10
+
11
+ func StartService() {
12
+ router := gin.Default()
13
+ router.Use(gin.Recovery())
14
+
15
+ router.GET("/", controller.HomeController)
16
+ AuthRoute(router)
17
+ UserRoute(router)
18
+ EmailRoute(router)
19
+ OptionsRoute(router)
20
+ AcademyRoute(router)
21
+ QuizRoute(router)
22
+
23
+ err := router.Run(config.TCP_ADDRESS)
24
+ if err != nil {
25
+ log.Fatalf("Failed to run server: %v", err)
26
+ }
27
+ }
space/space/space/space/services/email_verification_service.go CHANGED
@@ -1,181 +1,101 @@
1
- package services
2
-
3
- import (
4
- "crypto/tls"
5
- "fmt"
6
- "log"
7
- "math/rand/v2"
8
- "net/smtp"
9
- "time"
10
-
11
- "api.qobiltu.id/config"
12
- "api.qobiltu.id/models"
13
- "api.qobiltu.id/repositories"
14
- uuid "github.com/satori/go.uuid"
15
- )
16
-
17
- type EmailVerificationService struct {
18
- Service[models.EmailVerification, models.EmailVerification]
19
- }
20
-
21
- func (s *EmailVerificationService) Create() {
22
- accountRepo := repositories.GetAccountById(s.Constructor.AccountID)
23
- if accountRepo.NoRecord {
24
- s.Error = accountRepo.RowsError
25
- s.Exception.DataNotFound = true
26
- s.Exception.Message = "There is no account data with given credentials!"
27
- return
28
- }
29
-
30
- remainingTime := time.Duration(config.EMAIL_VERIFICATION_DURATION) * time.Hour
31
- dueTime := CalculateDueTime(remainingTime)
32
-
33
- token := uint(rand.IntN(999999-100000) + 100000)
34
- s.Constructor.UUID = uuid.NewV4()
35
-
36
- repo := repositories.CreateEmailVerification(s.Constructor.UUID, s.Constructor.AccountID, dueTime, token)
37
-
38
- s.Error = repo.RowsError
39
- s.Result = repo.Result
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. Auth
79
- auth := smtp.PlainAuth("", from, password, smtpHost)
80
- if err = c.Auth(auth); err != nil {
81
- s.Error = err
82
- log.Printf("Error auth mail: %v", 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)
90
- return
91
- }
92
-
93
- for _, addr := range to {
94
- if err = c.Rcpt(addr); err != nil {
95
- s.Error = err
96
- log.Printf("Error receipt addr: %v", err)
97
- return
98
- }
99
- }
100
-
101
- // 4. Send message
102
- wc, err := c.Data()
103
- if err != nil {
104
- s.Error = err
105
- log.Printf("Error data Send Message: %v", err)
106
- return
107
- }
108
-
109
- _, err = wc.Write(msg)
110
- if err != nil {
111
- s.Error = err
112
- log.Printf("Error write Send Message: %v", err)
113
- return
114
- }
115
-
116
- err = wc.Close()
117
- if err != nil {
118
- s.Error = err
119
- log.Printf("Error close Send Message: %v", err)
120
- return
121
- }
122
-
123
- c.Quit()
124
- fmt.Println("Email sent successfully!")
125
-
126
- // auth := smtp.PlainAuth("", from, password, smtpHost)
127
-
128
- // subject := "Email Verification Token"
129
- // body := fmt.Sprintf("Your verification token is: %06d\nPlease use it before it expires.", token)
130
-
131
- // msg := []byte("To: " + toEmail + "\r\n" +
132
- // "Subject: " + subject + "\r\n" +
133
- // "\r\n" +
134
- // body + "\r\n")
135
-
136
- // err := smtp.SendMail(smtpHost+":"+smtpPort, auth, from, []string{toEmail}, msg)
137
- // if err != nil {
138
- // s.Error = err
139
- // log.Printf("Error sending verification email: %v", err)
140
- // return
141
- // } else {
142
- // log.Printf("Successfully sending verification email: %v", err)
143
- // }
144
- }(accountRepo.Result.Email, token)
145
- // s.Result.Token = 0
146
- }
147
-
148
- func (s *EmailVerificationService) Validate() {
149
- repo := repositories.GetEmailVerification(s.Constructor.AccountID, s.Constructor.Token)
150
- s.Error = repo.RowsError
151
-
152
- if repo.NoRecord {
153
- s.Exception.DataNotFound = true
154
- s.Exception.Message = "Invalid token!"
155
- return
156
- }
157
-
158
- if repo.Result.ExpiredAt.Before(time.Now()) {
159
- s.Exception.Unauthorized = true
160
- s.Exception.Message = "Token has expired!"
161
- repositories.UpdateExpiredEmailVerification(s.Constructor.UUID)
162
- s.Delete()
163
- return
164
- }
165
- account := repositories.GetAccountById(repo.Result.AccountID)
166
- account.Result.IsEmailVerified = true
167
-
168
- repositories.UpdateAccount(account.Result)
169
- s.Result = repo.Result
170
- }
171
-
172
- func (s *EmailVerificationService) Delete() {
173
- repo := repositories.DeleteEmailVerification(s.Constructor.Token)
174
- s.Error = repo.RowsError
175
- if repo.NoRecord {
176
- s.Exception.DataNotFound = true
177
- s.Exception.Message = "Invalid token!"
178
- return
179
- }
180
- s.Result = repo.Result
181
- }
 
1
+ package services
2
+
3
+ import (
4
+ "api.qobiltu.id/utils"
5
+ "api.qobiltu.id/worker"
6
+ "context"
7
+ "github.com/hibiken/asynq"
8
+ "strconv"
9
+ "time"
10
+
11
+ "api.qobiltu.id/config"
12
+ "api.qobiltu.id/models"
13
+ "api.qobiltu.id/repositories"
14
+ uuid "github.com/satori/go.uuid"
15
+ )
16
+
17
+ type EmailVerificationService struct {
18
+ Service[models.EmailVerification, models.EmailVerification]
19
+ }
20
+
21
+ func (s *EmailVerificationService) Create() {
22
+ accountRepo := repositories.GetAccountById(s.Constructor.AccountID)
23
+ if accountRepo.NoRecord {
24
+ s.Error = accountRepo.RowsError
25
+ s.Exception.DataNotFound = true
26
+ s.Exception.Message = "There is no account data with given credentials!"
27
+ return
28
+ }
29
+
30
+ token, err := utils.GenerateToken()
31
+ if err != nil {
32
+ s.Error = err
33
+ s.Exception.InternalServerError = true
34
+ s.Exception.Message = "failed to generate token for email verification"
35
+ return
36
+ }
37
+
38
+ remainingTime := time.Duration(config.EMAIL_VERIFICATION_DURATION) * time.Hour
39
+ dueTime := CalculateDueTime(remainingTime)
40
+ s.Constructor.UUID = uuid.NewV4()
41
+ repo := repositories.CreateEmailVerification(s.Constructor.UUID, s.Constructor.AccountID, dueTime, uint(token))
42
+ s.Error = repo.RowsError
43
+ s.Result = repo.Result
44
+ if s.Error != nil {
45
+ return
46
+ }
47
+
48
+ err = worker.AsyncTaskDistributor.DistributeTaskSendVerifyEmail(
49
+ context.Background(),
50
+ &worker.PayloadSendVerifyEmail{
51
+ EmailAddress: accountRepo.Result.Email,
52
+ VerificationCode: strconv.Itoa(int(token)),
53
+ ExpirationInMinutes: int(remainingTime.Minutes()),
54
+ Subject: worker.TaskSendVerifyEmailSubject,
55
+ },
56
+ []asynq.Option{
57
+ asynq.MaxRetry(worker.TaskSendVerifyEmailMaxRetry),
58
+ asynq.Queue(worker.Critical),
59
+ }...)
60
+ if err != nil {
61
+ s.Error = err
62
+ s.Exception.InternalServerError = true
63
+ s.Exception.Message = "failed to send email verification"
64
+ return
65
+ }
66
+ }
67
+
68
+ func (s *EmailVerificationService) Validate() {
69
+ repo := repositories.GetEmailVerification(s.Constructor.AccountID, s.Constructor.Token)
70
+ s.Error = repo.RowsError
71
+
72
+ if repo.NoRecord {
73
+ s.Exception.DataNotFound = true
74
+ s.Exception.Message = "Invalid token!"
75
+ return
76
+ }
77
+
78
+ if repo.Result.ExpiredAt.Before(time.Now()) {
79
+ s.Exception.Unauthorized = true
80
+ s.Exception.Message = "Token has expired!"
81
+ repositories.UpdateExpiredEmailVerification(s.Constructor.UUID)
82
+ s.Delete()
83
+ return
84
+ }
85
+ account := repositories.GetAccountById(repo.Result.AccountID)
86
+ account.Result.IsEmailVerified = true
87
+
88
+ repositories.UpdateAccount(account.Result)
89
+ s.Result = repo.Result
90
+ }
91
+
92
+ func (s *EmailVerificationService) Delete() {
93
+ repo := repositories.DeleteEmailVerification(s.Constructor.Token)
94
+ s.Error = repo.RowsError
95
+ if repo.NoRecord {
96
+ s.Exception.DataNotFound = true
97
+ s.Exception.Message = "Invalid token!"
98
+ return
99
+ }
100
+ s.Result = repo.Result
101
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
space/space/space/space/services/forgot_password_service.go CHANGED
@@ -1,113 +1,114 @@
1
- package services
2
-
3
- import (
4
- "fmt"
5
- "log"
6
- "math/rand/v2"
7
- "net/smtp"
8
- "time"
9
-
10
- "api.qobiltu.id/config"
11
- "api.qobiltu.id/models"
12
- "api.qobiltu.id/repositories"
13
- uuid "github.com/satori/go.uuid"
14
- )
15
-
16
- type ForgotPasswordService struct {
17
- Service[models.ForgotPassword, models.ForgotPassword]
18
- }
19
-
20
- func (s *ForgotPasswordService) Create(email string) {
21
- if email == "" {
22
- s.Exception.BadRequest = true
23
- s.Exception.Message = "Email is required!"
24
- return
25
- }
26
- accountRepo := repositories.GetAccountbyEmail(email)
27
- if accountRepo.NoRecord {
28
- s.Error = accountRepo.RowsError
29
- s.Exception.DataNotFound = true
30
- s.Exception.Message = "There is no account data with given credentials!"
31
- return
32
- }
33
-
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
41
- s.Constructor.Token = token
42
- repo := repositories.CreateForgotPassword(s.Constructor)
43
-
44
- s.Error = repo.RowsError
45
- s.Result = repo.Result
46
- // Kirim token ke email user menggunakan SMTP
47
- go func(toEmail string, token uint) {
48
- from := config.SMTP_SENDER_EMAIL
49
- password := config.SMTP_SENDER_PASSWORD
50
- smtpHost := config.SMTP_HOST
51
- smtpPort := config.SMTP_PORT
52
-
53
- auth := smtp.PlainAuth("", from, password, smtpHost)
54
-
55
- subject := "Forgot Password Token"
56
- body := fmt.Sprintf("Your Forgot Password token is: %06d\nPlease use it before it expires.", token)
57
-
58
- msg := []byte("To: " + toEmail + "\r\n" +
59
- "Subject: " + subject + "\r\n" +
60
- "\r\n" +
61
- body + "\r\n")
62
-
63
- err := smtp.SendMail(smtpHost+":"+smtpPort, auth, from, []string{toEmail}, msg)
64
- if err != nil {
65
- s.Error = err
66
- log.Printf("Error sending verification email: %v", err)
67
- return
68
- }
69
- }(accountRepo.Result.Email, token)
70
- // s.Result.Token = 0
71
- }
72
-
73
- func (s *ForgotPasswordService) Validate(newPassword *string) {
74
-
75
- fgPasswordRepo := repositories.GetForgotPasswordByToken(s.Constructor.Token)
76
- s.Error = fgPasswordRepo.RowsError
77
- if fgPasswordRepo.NoRecord {
78
- s.Exception.DataNotFound = true
79
- s.Exception.Message = "There is no forgot password data with given credentials!"
80
- return
81
- }
82
- if fgPasswordRepo.Result.ExpiredAt.Before(time.Now()) {
83
- s.Exception.Unauthorized = true
84
- s.Exception.Message = "Token has expired!"
85
- return
86
- }
87
-
88
- accountRepo := repositories.GetAccountById(fgPasswordRepo.Result.AccountID)
89
- if accountRepo.NoRecord {
90
- s.Error = accountRepo.RowsError
91
- s.Exception.DataNotFound = true
92
- s.Exception.Message = "There is no account data with given credentials!"
93
- return
94
- }
95
- s.Result = fgPasswordRepo.Result
96
- if newPassword == nil {
97
- return
98
- }
99
- // fmt.Println("Previous Account", accountRepo.Result)
100
- // fmt.Println("New password", *newPassword)
101
- hashed_password, _ := HashPassword(*newPassword)
102
- accountRepo.Result.Password = hashed_password
103
- changePassword := repositories.UpdateAccount(accountRepo.Result)
104
- // fmt.Println("New Account", changePassword.Result)
105
- if changePassword.RowsError != nil {
106
- s.Error = changePassword.RowsError
107
- s.Exception.QueryError = true
108
- s.Exception.Message = "Failed to update password!"
109
- return
110
- }
111
- // fgPasswordRepo.Result.Token = 0
112
-
113
- }
 
 
1
+ package services
2
+
3
+ import (
4
+ "api.qobiltu.id/utils"
5
+ "api.qobiltu.id/worker"
6
+ "context"
7
+ "github.com/hibiken/asynq"
8
+ "strconv"
9
+ "time"
10
+
11
+ "api.qobiltu.id/config"
12
+ "api.qobiltu.id/models"
13
+ "api.qobiltu.id/repositories"
14
+ uuid "github.com/satori/go.uuid"
15
+ )
16
+
17
+ type ForgotPasswordService struct {
18
+ Service[models.ForgotPassword, models.ForgotPassword]
19
+ }
20
+
21
+ func (s *ForgotPasswordService) Create(email string) {
22
+ if email == "" {
23
+ s.Exception.BadRequest = true
24
+ s.Exception.Message = "Email is required!"
25
+ return
26
+ }
27
+ accountRepo := repositories.GetAccountbyEmail(email)
28
+ if accountRepo.NoRecord {
29
+ s.Error = accountRepo.RowsError
30
+ s.Exception.DataNotFound = true
31
+ s.Exception.Message = "There is no account data with given credentials!"
32
+ return
33
+ }
34
+
35
+ token, err := utils.GenerateToken()
36
+ if err != nil {
37
+ s.Error = err
38
+ s.Exception.InternalServerError = true
39
+ s.Exception.Message = "failed to generate token for email verification"
40
+ return
41
+ }
42
+
43
+ remainingTime := time.Duration(config.EMAIL_VERIFICATION_DURATION) * time.Hour
44
+ dueTime := CalculateDueTime(remainingTime)
45
+
46
+ s.Constructor.UUID = uuid.NewV4()
47
+ s.Constructor.ExpiredAt = dueTime
48
+ s.Constructor.AccountID = accountRepo.Result.Id
49
+ s.Constructor.Token = uint(token)
50
+ repo := repositories.CreateForgotPassword(s.Constructor)
51
+ s.Error = repo.RowsError
52
+ s.Result = repo.Result
53
+
54
+ err = worker.AsyncTaskDistributor.DistributeTaskSendForgotPasswordEmail(
55
+ context.Background(),
56
+ &worker.PayloadSendForgotPasswordEmail{
57
+ EmailAddress: accountRepo.Result.Email,
58
+ ResetToken: strconv.Itoa(int(token)),
59
+ ExpirationInMinutes: int(remainingTime.Minutes()),
60
+ Subject: worker.TaskSendForgotPasswordEmailSubject,
61
+ },
62
+ []asynq.Option{
63
+ asynq.MaxRetry(worker.TaskSendForgotPasswordEmailMaxRetry),
64
+ asynq.Queue(worker.Critical),
65
+ }...)
66
+ if err != nil {
67
+ s.Error = err
68
+ s.Exception.InternalServerError = true
69
+ s.Exception.Message = "failed to send email verification for forgot password request"
70
+ return
71
+ }
72
+ }
73
+
74
+ func (s *ForgotPasswordService) Validate(newPassword *string) {
75
+
76
+ fgPasswordRepo := repositories.GetForgotPasswordByToken(s.Constructor.Token)
77
+ s.Error = fgPasswordRepo.RowsError
78
+ if fgPasswordRepo.NoRecord {
79
+ s.Exception.DataNotFound = true
80
+ s.Exception.Message = "There is no forgot password data with given credentials!"
81
+ return
82
+ }
83
+ if fgPasswordRepo.Result.ExpiredAt.Before(time.Now()) {
84
+ s.Exception.Unauthorized = true
85
+ s.Exception.Message = "Token has expired!"
86
+ return
87
+ }
88
+
89
+ accountRepo := repositories.GetAccountById(fgPasswordRepo.Result.AccountID)
90
+ if accountRepo.NoRecord {
91
+ s.Error = accountRepo.RowsError
92
+ s.Exception.DataNotFound = true
93
+ s.Exception.Message = "There is no account data with given credentials!"
94
+ return
95
+ }
96
+ s.Result = fgPasswordRepo.Result
97
+ if newPassword == nil {
98
+ return
99
+ }
100
+ // fmt.Println("Previous Account", accountRepo.Result)
101
+ // fmt.Println("New password", *newPassword)
102
+ hashed_password, _ := HashPassword(*newPassword)
103
+ accountRepo.Result.Password = hashed_password
104
+ changePassword := repositories.UpdateAccount(accountRepo.Result)
105
+ // fmt.Println("New Account", changePassword.Result)
106
+ if changePassword.RowsError != nil {
107
+ s.Error = changePassword.RowsError
108
+ s.Exception.QueryError = true
109
+ s.Exception.Message = "Failed to update password!"
110
+ return
111
+ }
112
+ // fgPasswordRepo.Result.Token = 0
113
+
114
+ }