lifedebugger commited on
Commit
48471f7
·
1 Parent(s): 98c95a0

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. .github/workflows/main.yml +1 -0
  2. apperror/apperror.go +168 -0
  3. config/database_connection_config.go +80 -79
  4. config/tx.go +34 -0
  5. controller/health_check/health_check_controller.go +31 -0
  6. main.go +16 -1
  7. models/exception_model.go +17 -15
  8. models/health_check_model.go +10 -0
  9. repositories/health_check_repository.go +41 -0
  10. response/paging.go +58 -0
  11. response/response.go +152 -0
  12. router/health_check_route.go +5 -0
  13. router/router.go +12 -19
  14. router/server.go +32 -0
  15. services/health_check_service/health_check_service.go +15 -0
  16. services/health_check_service/health_check_service_check.go +21 -0
  17. services/register_service.go +66 -66
  18. space/.gitignore +2 -1
  19. space/Makefile +7 -0
  20. space/assets/efs.go +13 -0
  21. space/assets/emails/email-confirmation.tmpl +55 -0
  22. space/assets/emails/email-forgot-password.tmpl +56 -0
  23. space/config/config.go +63 -36
  24. space/controller/api_response.go +53 -0
  25. space/controller/controller.go +65 -65
  26. space/controller/email/email_create_verification_controller.go +21 -21
  27. space/docker-compose.dev.yml +18 -0
  28. space/docker-compose.yml +23 -0
  29. space/go.mod +9 -0
  30. space/go.sum +22 -0
  31. space/mail/sender.go +8 -0
  32. space/mail/smtp.go +71 -0
  33. space/main.go +62 -14
  34. space/middleware/authentication_middleware.go +37 -37
  35. space/router/router.go +27 -24
  36. space/services/email_verification_service.go +101 -181
  37. space/services/forgot_password_service.go +114 -113
  38. space/services/register_service.go +86 -86
  39. space/space/models/database_orm_model.go +4 -4
  40. space/space/services/academy_quiz_service.go +0 -1
  41. space/space/space/models/database_orm_model.go +3 -3
  42. space/space/space/space/controller/quiz/question_quiz_controller.go +1 -1
  43. space/space/space/space/models/database_orm_model.go +1 -1
  44. space/space/space/space/space/controller/quiz/answer_quiz_controller.go +2 -2
  45. space/space/space/space/space/controller/quiz/question_quiz_controller.go +5 -3
  46. space/space/space/space/space/space/controller/quiz/answer_quiz_controller.go +2 -1
  47. space/space/space/space/space/space/space/controller/quiz/attempt_quiz_controller.go +2 -1
  48. space/space/space/space/space/space/space/controller/quiz/submit_quiz_controller.go +23 -0
  49. space/space/space/space/space/space/space/models/database_orm_model.go +6 -0
  50. space/space/space/space/space/space/space/models/response_model.go +5 -0
.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
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
+ }
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
+ }
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
+ }
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
+ }
main.go CHANGED
@@ -2,8 +2,11 @@ package main
2
 
3
  import (
4
  "api.qobiltu.id/config"
 
5
  "api.qobiltu.id/mail"
 
6
  "api.qobiltu.id/router"
 
7
  "api.qobiltu.id/utils"
8
  "api.qobiltu.id/worker"
9
  "github.com/hibiken/asynq"
@@ -38,10 +41,22 @@ func main() {
38
  taskProcessor := worker.NewRedisTaskProcessor(asynqRedisOpt, emailSender)
39
  worker.AsyncTaskDistributor = taskDistributor
40
 
 
 
 
 
 
 
41
  err = taskProcessor.Start()
42
  utils.FatalIfErr("failed to start task processor", err)
43
  slog.Info("Task processor started")
44
 
 
 
 
 
 
45
  slog.Info("Starting server", "address", config.TCP_ADDRESS, "port", config.HOST_PORT)
46
- router.StartService()
 
47
  }
 
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"
 
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
  }
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
+ }
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
+ }
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
+ }
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
+ }
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
+ }
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
+ }
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
  }
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
+ }
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
+ }
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
+ }
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/.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/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/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/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/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/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/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/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.LogError(controller.Service.Error)
50
- utils.ResponseFAIL(c, 500, models.Exception{
51
- InternalServerError: true,
52
- Message: "Internal Server Error",
53
- })
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/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/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/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/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/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/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/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/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/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/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/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/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
+ }
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
- hasSpecial = false
24
- )
25
-
26
- if len(password) >= 8 {
27
- hasMinLen = true
28
- }
29
-
30
- for _, char := range password {
31
- switch {
32
- case unicode.IsUpper(char):
33
- hasUpper = true
34
- break
35
- case unicode.IsLower(char):
36
- hasLower = true
37
- break
38
- case unicode.IsDigit(char):
39
- hasNumber = true
40
- break
41
- case unicode.IsPunct(char) || unicode.IsSymbol(char):
42
- hasSpecial = true
43
- break
44
- }
45
- }
46
- approve := hasMinLen && hasUpper && hasLower && hasNumber && hasSpecial
47
- if !approve {
48
- return models.Exception{
49
- BadRequest: true,
50
- Message: "Password must contain at least 8 characters, including uppercase, lowercase, number, and special character!",
51
- }
52
- }
53
- return models.Exception{}
54
- }
55
-
56
- func (s *RegisterService) Create() {
57
- validatePassword := ValidatePassword(s.Constructor.Password)
58
- if validatePassword.BadRequest {
59
- s.Exception = validatePassword
60
- return
61
- }
62
- hashed_password, err_hash := HashPassword(s.Constructor.Password)
63
- s.Error = err_hash
64
- s.Constructor.Password = hashed_password
65
- s.Constructor.UUID = uuid.NewV4()
66
- accountCreated := repositories.CreateAccount(s.Constructor)
67
- if errors.Is(accountCreated.RowsError, gorm.ErrDuplicatedKey) {
68
- s.Exception.DataDuplicate = true
69
- s.Exception.Message = "Account with email " + s.Constructor.Email + " already exists!"
70
- return
71
- } 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) {
72
- s.Exception.BadRequest = true
73
- s.Exception.Message = "Bad request!"
74
- return
75
- }
76
- userProfile := UserProfileService{}
77
- userProfile.Constructor.AccountID = accountCreated.Result.Id
78
- userProfile.Create()
79
- if userProfile.Error != nil {
80
- s.Error = userProfile.Error
81
- return
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/models/database_orm_model.go CHANGED
@@ -178,10 +178,10 @@ type UserAnswer struct {
178
  IsCorrect bool `json:"is_correct"`
179
  }
180
  type QuizResult struct {
181
- QuizAttemptID uint `gorm:"column:quiz_attempt_id"`
182
- TotalQuestions int `gorm:"column:total_questions"`
183
- CorrectAnswers int `gorm:"column:correct_answers"`
184
- AverageScore float64 `gorm:"column:average_score"`
185
  }
186
 
187
  // Gorm table name settings
 
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
space/space/services/academy_quiz_service.go CHANGED
@@ -8,7 +8,6 @@ import (
8
  "api.qobiltu.id/models"
9
  "api.qobiltu.id/repositories"
10
  )
11
-
12
  type AttemptQuizService struct {
13
  Service[models.Quiz, models.QuizAttempt]
14
  }
 
8
  "api.qobiltu.id/models"
9
  "api.qobiltu.id/repositories"
10
  )
 
11
  type AttemptQuizService struct {
12
  Service[models.Quiz, models.QuizAttempt]
13
  }
space/space/space/models/database_orm_model.go CHANGED
@@ -138,7 +138,7 @@ type RegionCity struct {
138
  ProvinceID uint `json:"province_id"`
139
  }
140
  type Answer struct {
141
- ID uint `gorm:"primaryKey"`
142
  QuestionID uint `json:"question_id"`
143
  Content string `json:"content"`
144
  IsCorrect bool `json:"-"`
@@ -162,7 +162,7 @@ type Quiz struct {
162
  }
163
 
164
  type QuizAttempt struct {
165
- ID uint `gorm:"primaryKey"`
166
  AccountID uint `json:"user_id"`
167
  QuizID uint `json:"quiz_id"`
168
  StartedAt time.Time `json:"started_at"`
@@ -171,7 +171,7 @@ type QuizAttempt struct {
171
  Score float64 `json:"score"`
172
  }
173
  type UserAnswer struct {
174
- ID uint `gorm:"primaryKey"`
175
  QuizAttemptID uint `json:"quiz_attempt_id"`
176
  QuestionID uint `json:"question_id"`
177
  SelectedAnswer uint `json:"selected_answer"`
 
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:"-"`
 
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"`
 
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"`
space/space/space/space/controller/quiz/question_quiz_controller.go CHANGED
@@ -17,7 +17,7 @@ func Question(c *gin.Context) {
17
  questionQuizController.HeaderParse(c, func() {
18
  quizId, _ := strconv.Atoi(c.Param("quiz_id"))
19
  academyId, _ := strconv.Atoi(c.Param("academy_id"))
20
- questionNo, _ := strconv.Atoi(c.Query("question_no"))
21
  questionQuizController.Service.Constructor.ID = uint(quizId)
22
  questionQuizController.Service.Constructor.AcademyID = uint(academyId)
23
  questionQuiz.Retrieve(questionQuizController.AccountData.UserID, questionNo)
 
17
  questionQuizController.HeaderParse(c, func() {
18
  quizId, _ := strconv.Atoi(c.Param("quiz_id"))
19
  academyId, _ := strconv.Atoi(c.Param("academy_id"))
20
+ questionNo, _ := strconv.Atoi(c.Query("question_no"))
21
  questionQuizController.Service.Constructor.ID = uint(quizId)
22
  questionQuizController.Service.Constructor.AcademyID = uint(academyId)
23
  questionQuiz.Retrieve(questionQuizController.AccountData.UserID, questionNo)
space/space/space/space/models/database_orm_model.go CHANGED
@@ -144,7 +144,7 @@ type Answer struct {
144
  IsCorrect bool `json:"-"`
145
  }
146
  type Question struct {
147
- ID uint `gorm:"primaryKey"`
148
  QuizID uint `json:"quiz_id"`
149
  Content string `json:"content"`
150
  Order int `json:"order"`
 
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"`
space/space/space/space/space/controller/quiz/answer_quiz_controller.go CHANGED
@@ -17,9 +17,9 @@ func Answer(c *gin.Context) {
17
  quizAnswerController.RequestJSON(c, func() {
18
  quizId, _ := strconv.Atoi(c.Param("quiz_id"))
19
  academyId, _ := strconv.Atoi(c.Param("academy_id"))
20
- questionNo, _ := strconv.Atoi(c.Query("question_no"))
21
  quizAnswerController.Service.Constructor.ID = uint(quizId)
22
  quizAnswerController.Service.Constructor.AcademyID = uint(academyId)
23
- quizAnswer.Update(quizAnswerController.AccountData.UserID, questionNo, quizAnswerController.Request.Answer)
24
  })
25
  }
 
17
  quizAnswerController.RequestJSON(c, func() {
18
  quizId, _ := strconv.Atoi(c.Param("quiz_id"))
19
  academyId, _ := strconv.Atoi(c.Param("academy_id"))
20
+
21
  quizAnswerController.Service.Constructor.ID = uint(quizId)
22
  quizAnswerController.Service.Constructor.AcademyID = uint(academyId)
23
+ quizAnswer.Update(quizAnswerController.AccountData.UserID, quizAnswerController.Request.QuestionNo, quizAnswerController.Request.Answer)
24
  })
25
  }
space/space/space/space/space/controller/quiz/question_quiz_controller.go CHANGED
@@ -11,15 +11,17 @@ import (
11
 
12
  func Question(c *gin.Context) {
13
  questionQuiz := services.QuestionQuizService{}
14
- questionQuizController := controller.Controller[models.QuestionQuizRequest, models.Quiz, models.QuestionResponse]{
15
  Service: &questionQuiz.Service,
16
  }
17
- questionQuizController.RequestJSON(c, func() {
18
  quizId, _ := strconv.Atoi(c.Param("quiz_id"))
19
  academyId, _ := strconv.Atoi(c.Param("academy_id"))
 
20
  questionQuizController.Service.Constructor.ID = uint(quizId)
21
  questionQuizController.Service.Constructor.AcademyID = uint(academyId)
22
- questionQuiz.Retrieve(questionQuizController.AccountData.UserID, questionQuizController.Request.QuestionNo)
 
23
 
24
  })
25
  }
 
11
 
12
  func Question(c *gin.Context) {
13
  questionQuiz := services.QuestionQuizService{}
14
+ questionQuizController := controller.Controller[any, models.Quiz, models.QuestionResponse]{
15
  Service: &questionQuiz.Service,
16
  }
17
+ questionQuizController.HeaderParse(c, func() {
18
  quizId, _ := strconv.Atoi(c.Param("quiz_id"))
19
  academyId, _ := strconv.Atoi(c.Param("academy_id"))
20
+ questionNo, _ := strconv.Atoi(c.Query("question_no"))
21
  questionQuizController.Service.Constructor.ID = uint(quizId)
22
  questionQuizController.Service.Constructor.AcademyID = uint(academyId)
23
+ questionQuiz.Retrieve(questionQuizController.AccountData.UserID, questionNo)
24
+ questionQuizController.Response(c)
25
 
26
  })
27
  }
space/space/space/space/space/space/controller/quiz/answer_quiz_controller.go CHANGED
@@ -17,8 +17,9 @@ func Answer(c *gin.Context) {
17
  quizAnswerController.RequestJSON(c, func() {
18
  quizId, _ := strconv.Atoi(c.Param("quiz_id"))
19
  academyId, _ := strconv.Atoi(c.Param("academy_id"))
 
20
  quizAnswerController.Service.Constructor.ID = uint(quizId)
21
  quizAnswerController.Service.Constructor.AcademyID = uint(academyId)
22
- quizAnswer.Update(quizAnswerController.AccountData.UserID, quizAnswerController.Request.QuestionNo, quizAnswerController.Request.Answer)
23
  })
24
  }
 
17
  quizAnswerController.RequestJSON(c, func() {
18
  quizId, _ := strconv.Atoi(c.Param("quiz_id"))
19
  academyId, _ := strconv.Atoi(c.Param("academy_id"))
20
+ questionNo, _ := strconv.Atoi(c.Query("question_no"))
21
  quizAnswerController.Service.Constructor.ID = uint(quizId)
22
  quizAnswerController.Service.Constructor.AcademyID = uint(academyId)
23
+ quizAnswer.Update(quizAnswerController.AccountData.UserID, questionNo, quizAnswerController.Request.Answer)
24
  })
25
  }
space/space/space/space/space/space/space/controller/quiz/attempt_quiz_controller.go CHANGED
@@ -14,11 +14,12 @@ func Attempt(c *gin.Context) {
14
  attemptQuizController := controller.Controller[any, models.Quiz, models.QuizAttempt]{
15
  Service: &attemptQuiz.Service,
16
  }
17
- attemptQuizController.RequestJSON(c, func() {
18
  quizId, _ := strconv.Atoi(c.Param("quiz_id"))
19
  academyId, _ := strconv.Atoi(c.Param("academy_id"))
20
  attemptQuizController.Service.Constructor.ID = uint(quizId)
21
  attemptQuizController.Service.Constructor.AcademyID = uint(academyId)
22
  attemptQuiz.Create(attemptQuizController.AccountData.UserID)
 
23
  })
24
  }
 
14
  attemptQuizController := controller.Controller[any, models.Quiz, models.QuizAttempt]{
15
  Service: &attemptQuiz.Service,
16
  }
17
+ attemptQuizController.HeaderParse(c, func() {
18
  quizId, _ := strconv.Atoi(c.Param("quiz_id"))
19
  academyId, _ := strconv.Atoi(c.Param("academy_id"))
20
  attemptQuizController.Service.Constructor.ID = uint(quizId)
21
  attemptQuizController.Service.Constructor.AcademyID = uint(academyId)
22
  attemptQuiz.Create(attemptQuizController.AccountData.UserID)
23
+ attemptQuizController.Response(c)
24
  })
25
  }
space/space/space/space/space/space/space/controller/quiz/submit_quiz_controller.go ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package controller
2
+
3
+ import (
4
+ "strconv"
5
+
6
+ "api.qobiltu.id/controller"
7
+ "api.qobiltu.id/models"
8
+ "api.qobiltu.id/services"
9
+ "github.com/gin-gonic/gin"
10
+ )
11
+
12
+ func Submit(c *gin.Context) {
13
+ submitQuiz := services.SubmitQuizService{}
14
+ submitQuizController := controller.Controller[any, models.QuizAttempt, models.QuizResultResponse]{
15
+ Service: &submitQuiz.Service,
16
+ }
17
+ submitQuizController.HeaderParse(c, func() {
18
+ quizId, _ := strconv.Atoi(c.Param("attempt_id"))
19
+ submitQuizController.Service.Constructor.ID = uint(quizId)
20
+ submitQuiz.Create(submitQuizController.AccountData.UserID)
21
+ submitQuizController.Response(c)
22
+ })
23
+ }
space/space/space/space/space/space/space/models/database_orm_model.go CHANGED
@@ -177,6 +177,12 @@ type UserAnswer struct {
177
  SelectedAnswer uint `json:"selected_answer"`
178
  IsCorrect bool `json:"is_correct"`
179
  }
 
 
 
 
 
 
180
 
181
  // Gorm table name settings
182
  func (Account) TableName() string { return "account" }
 
177
  SelectedAnswer uint `json:"selected_answer"`
178
  IsCorrect bool `json:"is_correct"`
179
  }
180
+ type QuizResult struct {
181
+ QuizAttemptID uint `gorm:"column:quiz_attempt_id"`
182
+ TotalQuestions int `gorm:"column:total_questions"`
183
+ CorrectAnswers int `gorm:"column:correct_answers"`
184
+ AverageScore float64 `gorm:"column:average_score"`
185
+ }
186
 
187
  // Gorm table name settings
188
  func (Account) TableName() string { return "account" }
space/space/space/space/space/space/space/models/response_model.go CHANGED
@@ -54,3 +54,8 @@ type QuestionResponse struct {
54
  Answer []Answer `json:"answer_options"`
55
  UserAnswer int `json:"current_user_answer"`
56
  }
 
 
 
 
 
 
54
  Answer []Answer `json:"answer_options"`
55
  UserAnswer int `json:"current_user_answer"`
56
  }
57
+
58
+ type QuizResultResponse struct {
59
+ QuizAttempt QuizAttempt `json:"quiz_attempt"`
60
+ Result QuizResult `json:"result"`
61
+ }