Spaces:
Configuration error
Configuration error
Commit ·
48471f7
1
Parent(s): 98c95a0
Deploy files from GitHub repository
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .github/workflows/main.yml +1 -0
- apperror/apperror.go +168 -0
- config/database_connection_config.go +80 -79
- config/tx.go +34 -0
- controller/health_check/health_check_controller.go +31 -0
- main.go +16 -1
- models/exception_model.go +17 -15
- models/health_check_model.go +10 -0
- repositories/health_check_repository.go +41 -0
- response/paging.go +58 -0
- response/response.go +152 -0
- router/health_check_route.go +5 -0
- router/router.go +12 -19
- router/server.go +32 -0
- services/health_check_service/health_check_service.go +15 -0
- services/health_check_service/health_check_service_check.go +21 -0
- services/register_service.go +66 -66
- space/.gitignore +2 -1
- space/Makefile +7 -0
- space/assets/efs.go +13 -0
- space/assets/emails/email-confirmation.tmpl +55 -0
- space/assets/emails/email-forgot-password.tmpl +56 -0
- space/config/config.go +63 -36
- space/controller/api_response.go +53 -0
- space/controller/controller.go +65 -65
- space/controller/email/email_create_verification_controller.go +21 -21
- space/docker-compose.dev.yml +18 -0
- space/docker-compose.yml +23 -0
- space/go.mod +9 -0
- space/go.sum +22 -0
- space/mail/sender.go +8 -0
- space/mail/smtp.go +71 -0
- space/main.go +62 -14
- space/middleware/authentication_middleware.go +37 -37
- space/router/router.go +27 -24
- space/services/email_verification_service.go +101 -181
- space/services/forgot_password_service.go +114 -113
- space/services/register_service.go +86 -86
- space/space/models/database_orm_model.go +4 -4
- space/space/services/academy_quiz_service.go +0 -1
- space/space/space/models/database_orm_model.go +3 -3
- space/space/space/space/controller/quiz/question_quiz_controller.go +1 -1
- space/space/space/space/models/database_orm_model.go +1 -1
- space/space/space/space/space/controller/quiz/answer_quiz_controller.go +2 -2
- space/space/space/space/space/controller/quiz/question_quiz_controller.go +5 -3
- space/space/space/space/space/space/controller/quiz/answer_quiz_controller.go +2 -1
- space/space/space/space/space/space/space/controller/quiz/attempt_quiz_controller.go +2 -1
- space/space/space/space/space/space/space/controller/quiz/submit_quiz_controller.go +23 -0
- space/space/space/space/space/space/space/models/database_orm_model.go +6 -0
- 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 |
-
"
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
"gorm.io/
|
| 10 |
-
"gorm.io/gorm
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
"
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
var
|
| 18 |
-
var
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
AutoMigrateAll
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
&models.
|
| 54 |
-
&models.
|
| 55 |
-
&models.
|
| 56 |
-
&models.
|
| 57 |
-
&models.
|
| 58 |
-
&models.
|
| 59 |
-
&models.
|
| 60 |
-
&models.
|
| 61 |
-
&models.
|
| 62 |
-
&models.
|
| 63 |
-
&models.
|
| 64 |
-
&models.
|
| 65 |
-
&models.
|
| 66 |
-
&models.
|
| 67 |
-
&models.
|
| 68 |
-
&models.
|
| 69 |
-
&models.
|
| 70 |
-
&models.
|
| 71 |
-
&models.
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 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 |
-
|
|
|
|
| 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 |
-
|
| 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 |
-
|
| 5 |
-
|
| 6 |
-
"api.qobiltu.id/config"
|
| 7 |
-
"api.qobiltu.id/controller"
|
| 8 |
-
"github.com/gin-gonic/gin"
|
| 9 |
)
|
| 10 |
|
| 11 |
-
func
|
| 12 |
-
|
| 13 |
-
|
| 14 |
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
QuizRoute(router)
|
| 22 |
|
| 23 |
-
|
| 24 |
-
|
| 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 |
-
|
| 5 |
-
|
| 6 |
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
)
|
| 12 |
|
| 13 |
type RegisterService struct {
|
| 14 |
-
|
| 15 |
}
|
| 16 |
|
| 17 |
func ValidatePassword(password string) models.Exception {
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
}
|
| 51 |
|
| 52 |
func (s *RegisterService) Create() {
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 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 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
var
|
| 12 |
-
var
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
var
|
| 16 |
-
var
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
var
|
| 20 |
-
var
|
| 21 |
-
var
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 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 |
-
|
| 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 |
-
|
| 51 |
-
InternalServerError: true,
|
| 52 |
-
Message: "Internal Server Error",
|
| 53 |
-
})
|
| 54 |
-
case controller.Service.Exception.DataDuplicate:
|
| 55 |
-
|
| 56 |
-
case controller.Service.Exception.Unauthorized:
|
| 57 |
-
|
| 58 |
-
case controller.Service.Exception.DataNotFound:
|
| 59 |
-
|
| 60 |
-
case controller.Service.Exception.Message != "":
|
| 61 |
-
|
| 62 |
-
default:
|
| 63 |
-
|
| 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 =
|
| 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 |
-
"
|
| 5 |
-
|
| 6 |
-
"api.qobiltu.id/
|
| 7 |
-
"api.qobiltu.id/
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 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/
|
| 7 |
-
"api.qobiltu.id/
|
| 8 |
-
"api.qobiltu.id/
|
| 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 |
-
|
| 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 |
-
|
| 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.
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 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 |
-
"
|
| 5 |
-
"
|
| 6 |
-
"
|
| 7 |
-
"
|
| 8 |
-
"
|
| 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 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 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 |
-
"
|
| 5 |
-
"
|
| 6 |
-
"
|
| 7 |
-
"
|
| 8 |
-
"
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
"api.qobiltu.id/
|
| 12 |
-
"api.qobiltu.id/
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
s.Exception.
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
s.
|
| 30 |
-
s.Exception.
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
}
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
s.Exception.
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
s.Exception.
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
s.
|
| 92 |
-
s.Exception.
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
// fmt.Println("
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
s.
|
| 108 |
-
s.Exception.
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 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 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
)
|
| 12 |
-
|
| 13 |
-
type RegisterService struct {
|
| 14 |
-
|
| 15 |
-
}
|
| 16 |
-
|
| 17 |
-
func ValidatePassword(password string) models.Exception {
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 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 |
-
|
| 21 |
quizAnswerController.Service.Constructor.ID = uint(quizId)
|
| 22 |
quizAnswerController.Service.Constructor.AcademyID = uint(academyId)
|
| 23 |
-
quizAnswer.Update(quizAnswerController.AccountData.UserID,
|
| 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[
|
| 15 |
Service: &questionQuiz.Service,
|
| 16 |
}
|
| 17 |
-
questionQuizController.
|
| 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,
|
|
|
|
| 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,
|
| 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.
|
| 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 |
+
}
|