Spaces:
Runtime error
Runtime error
RyZ
commited on
Commit
·
44c4b7e
1
Parent(s):
d690ed7
feat: making register, login authentication and connect to whatsapp by terminal qrcode
Browse files- .env.example +10 -0
- config/database_config.go +2 -14
- config/env_config.go +33 -1
- controllers/auth_controller.go +53 -0
- controllers/connection_controller.go +89 -0
- go.mod +18 -2
- go.sum +53 -0
- middleware/auth_middleware.go +58 -0
- models/dto/auth_dto.go +19 -0
- models/dto/connection_dto.go +19 -0
- models/dto/http_response_dto.go +2 -2
- models/entity/constant.go +11 -0
- models/entity/entity.go +21 -0
- models/entity/user_entity.go +21 -0
- models/error/error.go +16 -0
- provider/controller_provider.go +22 -1
- provider/middleware_provider.go +14 -2
- provider/provider.go +9 -2
- provider/repositories_provider.go +22 -1
- provider/services_provider.go +21 -1
- repositories/auth_repository.go +32 -0
- repositories/connection_repositories.go +121 -0
- repositories/repositories.go +1 -0
- router/auth_router.go +18 -0
- router/connection_router.go +20 -0
- router/router.go +9 -2
- services/auth_service.go +87 -0
- services/connection_service.go +108 -0
- services/service.go +1 -1
- utils/jwt_util.go +28 -0
- utils/password_util.go +13 -0
- utils/response_util.go +52 -1
.env.example
CHANGED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
DB_HOST =
|
| 2 |
+
DB_USER =
|
| 3 |
+
DB_PASSWORD =
|
| 4 |
+
DB_PORT =
|
| 5 |
+
DB_NAME =
|
| 6 |
+
HOST_ADDRESS =
|
| 7 |
+
HOST_PORT =
|
| 8 |
+
SALT =
|
| 9 |
+
LOG_PATH =
|
| 10 |
+
JWT_SECRET_KEY =
|
config/database_config.go
CHANGED
|
@@ -3,7 +3,6 @@ package config
|
|
| 3 |
import (
|
| 4 |
"fmt"
|
| 5 |
"log"
|
| 6 |
-
"strings"
|
| 7 |
|
| 8 |
"gorm.io/driver/postgres"
|
| 9 |
"gorm.io/gorm"
|
|
@@ -13,24 +12,13 @@ type DatabaseConfig interface {
|
|
| 13 |
AutoMigrateAll(entities ...interface{}) error
|
| 14 |
GetInstance() *gorm.DB
|
| 15 |
}
|
| 16 |
-
|
| 17 |
type databaseConfig struct {
|
| 18 |
db *gorm.DB
|
| 19 |
}
|
| 20 |
|
| 21 |
func NewDatabaseConfig(DB_HOST, DB_USER, DB_PASSWORD, DB_NAME, DB_PORT string) DatabaseConfig {
|
| 22 |
-
// Clean inputs to remove accidental whitespace from secrets
|
| 23 |
-
DB_HOST = strings.TrimSpace(DB_HOST)
|
| 24 |
-
DB_USER = strings.TrimSpace(DB_USER)
|
| 25 |
-
DB_PASSWORD = strings.TrimSpace(DB_PASSWORD)
|
| 26 |
-
DB_NAME = strings.TrimSpace(DB_NAME)
|
| 27 |
-
DB_PORT = strings.TrimSpace(DB_PORT)
|
| 28 |
-
|
| 29 |
-
// Debug logging to see connection details (quoted to spot hidden spaces)
|
| 30 |
-
log.Printf("Connecting to DB: Host='%s' User='%s' Port='%s' DBName='%s'", DB_HOST, DB_USER, DB_PORT, DB_NAME)
|
| 31 |
-
|
| 32 |
dsn := fmt.Sprintf(
|
| 33 |
-
"host=%s user=%s password=%s dbname=%s port=%s TimeZone=Asia/Jakarta ",
|
| 34 |
DB_HOST, DB_USER, DB_PASSWORD, DB_NAME, DB_PORT,
|
| 35 |
)
|
| 36 |
|
|
@@ -61,4 +49,4 @@ func (cfg *databaseConfig) AutoMigrateAll(entities ...interface{}) error {
|
|
| 61 |
|
| 62 |
func (cfg *databaseConfig) GetInstance() *gorm.DB {
|
| 63 |
return cfg.db
|
| 64 |
-
}
|
|
|
|
| 3 |
import (
|
| 4 |
"fmt"
|
| 5 |
"log"
|
|
|
|
| 6 |
|
| 7 |
"gorm.io/driver/postgres"
|
| 8 |
"gorm.io/gorm"
|
|
|
|
| 12 |
AutoMigrateAll(entities ...interface{}) error
|
| 13 |
GetInstance() *gorm.DB
|
| 14 |
}
|
|
|
|
| 15 |
type databaseConfig struct {
|
| 16 |
db *gorm.DB
|
| 17 |
}
|
| 18 |
|
| 19 |
func NewDatabaseConfig(DB_HOST, DB_USER, DB_PASSWORD, DB_NAME, DB_PORT string) DatabaseConfig {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
dsn := fmt.Sprintf(
|
| 21 |
+
"host=%s user=%s password=%s dbname=%s port=%s sslmode=disable TimeZone=Asia/Jakarta ",
|
| 22 |
DB_HOST, DB_USER, DB_PASSWORD, DB_NAME, DB_PORT,
|
| 23 |
)
|
| 24 |
|
|
|
|
| 49 |
|
| 50 |
func (cfg *databaseConfig) GetInstance() *gorm.DB {
|
| 51 |
return cfg.db
|
| 52 |
+
}
|
config/env_config.go
CHANGED
|
@@ -2,6 +2,9 @@ package config
|
|
| 2 |
|
| 3 |
import (
|
| 4 |
"os"
|
|
|
|
|
|
|
|
|
|
| 5 |
"github.com/joho/godotenv"
|
| 6 |
)
|
| 7 |
|
|
@@ -10,12 +13,16 @@ type EnvConfig interface {
|
|
| 10 |
GetLogPath() string
|
| 11 |
GetHostAddress() string
|
| 12 |
GetHostPort() string
|
|
|
|
| 13 |
GetDatabaseHost() string
|
| 14 |
GetDatabasePort() string
|
| 15 |
GetDatabaseUser() string
|
| 16 |
GetDatabasePassword() string
|
| 17 |
GetDatabaseName() string
|
| 18 |
GetSalt() string
|
|
|
|
|
|
|
|
|
|
| 19 |
}
|
| 20 |
|
| 21 |
type envConfig struct {
|
|
@@ -31,7 +38,12 @@ func NewEnvConfig(timezone string) EnvConfig {
|
|
| 31 |
}
|
| 32 |
|
| 33 |
func (e *envConfig) GetTCPAddress() string {
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
}
|
| 36 |
|
| 37 |
func (e *envConfig) GetLogPath() string {
|
|
@@ -46,6 +58,14 @@ func (e *envConfig) GetHostPort() string {
|
|
| 46 |
return os.Getenv("HOST_PORT")
|
| 47 |
}
|
| 48 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
func (e *envConfig) GetDatabaseHost() string {
|
| 50 |
return os.Getenv("DB_HOST")
|
| 51 |
}
|
|
@@ -73,3 +93,15 @@ func (e *envConfig) GetSalt() string {
|
|
| 73 |
}
|
| 74 |
return salt
|
| 75 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
import (
|
| 4 |
"os"
|
| 5 |
+
"strconv"
|
| 6 |
+
"strings"
|
| 7 |
+
|
| 8 |
"github.com/joho/godotenv"
|
| 9 |
)
|
| 10 |
|
|
|
|
| 13 |
GetLogPath() string
|
| 14 |
GetHostAddress() string
|
| 15 |
GetHostPort() string
|
| 16 |
+
GetEmailVerificationDuration() int
|
| 17 |
GetDatabaseHost() string
|
| 18 |
GetDatabasePort() string
|
| 19 |
GetDatabaseUser() string
|
| 20 |
GetDatabasePassword() string
|
| 21 |
GetDatabaseName() string
|
| 22 |
GetSalt() string
|
| 23 |
+
GetSupabaseURL() string
|
| 24 |
+
GetSupabaseKey() string
|
| 25 |
+
GetSupabaseBucket() string
|
| 26 |
}
|
| 27 |
|
| 28 |
type envConfig struct {
|
|
|
|
| 38 |
}
|
| 39 |
|
| 40 |
func (e *envConfig) GetTCPAddress() string {
|
| 41 |
+
host := os.Getenv("HOST_ADDRESS")
|
| 42 |
+
port := os.Getenv("HOST_PORT")
|
| 43 |
+
if port == "" {
|
| 44 |
+
port = "8080"
|
| 45 |
+
}
|
| 46 |
+
return host + ":" + port
|
| 47 |
}
|
| 48 |
|
| 49 |
func (e *envConfig) GetLogPath() string {
|
|
|
|
| 58 |
return os.Getenv("HOST_PORT")
|
| 59 |
}
|
| 60 |
|
| 61 |
+
func (e *envConfig) GetEmailVerificationDuration() int {
|
| 62 |
+
duration, err := strconv.Atoi(os.Getenv("EMAIL_VERIFICATION_DURATION"))
|
| 63 |
+
if err != nil {
|
| 64 |
+
return 0 // Default value if parsing fails
|
| 65 |
+
}
|
| 66 |
+
return duration
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
func (e *envConfig) GetDatabaseHost() string {
|
| 70 |
return os.Getenv("DB_HOST")
|
| 71 |
}
|
|
|
|
| 93 |
}
|
| 94 |
return salt
|
| 95 |
}
|
| 96 |
+
|
| 97 |
+
func (e *envConfig) GetSupabaseURL() string {
|
| 98 |
+
return strings.TrimSpace(os.Getenv("SUPABASE_URL"))
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
func (e *envConfig) GetSupabaseKey() string {
|
| 102 |
+
return strings.TrimSpace(os.Getenv("SUPABASE_SERVICE_KEY"))
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
func (e *envConfig) GetSupabaseBucket() string {
|
| 106 |
+
return strings.TrimSpace(os.Getenv("SUPABASE_BUCKET_NAME"))
|
| 107 |
+
}
|
controllers/auth_controller.go
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package controllers
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"whatsapp-backend/models/dto"
|
| 5 |
+
"whatsapp-backend/services"
|
| 6 |
+
"whatsapp-backend/utils"
|
| 7 |
+
"github.com/gin-gonic/gin"
|
| 8 |
+
)
|
| 9 |
+
|
| 10 |
+
type AuthController interface {
|
| 11 |
+
Register(ctx *gin.Context)
|
| 12 |
+
Login(ctx *gin.Context)
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
type authController struct {
|
| 16 |
+
authService services.AuthService
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
func NewAuthController(authService services.AuthService) AuthController {
|
| 20 |
+
return &authController{authService: authService}
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
func (c *authController) Register(ctx *gin.Context) {
|
| 24 |
+
var req dto.RegisterRequest
|
| 25 |
+
if err := ctx.ShouldBindJSON(&req); err != nil {
|
| 26 |
+
utils.SendResponse[any, any](ctx, nil, nil, err)
|
| 27 |
+
return
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
resp, err := c.authService.Register(req)
|
| 31 |
+
if err != nil {
|
| 32 |
+
utils.SendResponse[any, any](ctx, nil, nil, err)
|
| 33 |
+
return
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
utils.SendResponse[dto.AuthResponse, any](ctx, nil, *resp, nil)
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
func (c *authController) Login(ctx *gin.Context) {
|
| 40 |
+
var req dto.LoginRequest
|
| 41 |
+
if err := ctx.ShouldBindJSON(&req); err != nil {
|
| 42 |
+
utils.SendResponse[any, any](ctx, nil, nil, err)
|
| 43 |
+
return
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
resp, err := c.authService.Login(req)
|
| 47 |
+
if err != nil {
|
| 48 |
+
utils.SendResponse[any, any](ctx, nil, nil, err)
|
| 49 |
+
return
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
utils.SendResponse[dto.AuthResponse, any](ctx, nil, *resp, nil)
|
| 53 |
+
}
|
controllers/connection_controller.go
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package controllers
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"context"
|
| 5 |
+
|
| 6 |
+
"github.com/gin-gonic/gin"
|
| 7 |
+
"github.com/google/uuid"
|
| 8 |
+
|
| 9 |
+
"whatsapp-backend/models/dto"
|
| 10 |
+
entity "whatsapp-backend/models/entity"
|
| 11 |
+
http_error "whatsapp-backend/models/error"
|
| 12 |
+
"whatsapp-backend/services"
|
| 13 |
+
"whatsapp-backend/utils"
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
type ConnectionController interface {
|
| 17 |
+
Connect(ctx *gin.Context)
|
| 18 |
+
GetStatus(ctx *gin.Context)
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
type connectionController struct {
|
| 22 |
+
connectionService services.ConnectionService
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
func NewConnectionController(connectionService services.ConnectionService) ConnectionController {
|
| 26 |
+
return &connectionController{connectionService}
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
func (cc *connectionController) Connect(ctx *gin.Context) {
|
| 30 |
+
// 1. Extract UserID from Context (set by AuthMiddleware)
|
| 31 |
+
userID, exists := ctx.Get("user_id")
|
| 32 |
+
if !exists {
|
| 33 |
+
utils.SendResponse[any, any](ctx, dto.AuthResponse{}, nil, http_error.UNAUTHORIZED)
|
| 34 |
+
return
|
| 35 |
+
}
|
| 36 |
+
accountID := userID.(uuid.UUID)
|
| 37 |
+
|
| 38 |
+
// 2. Bind JSON (Optional)
|
| 39 |
+
var req dto.ConnectRequest
|
| 40 |
+
_ = ctx.ShouldBindJSON(&req)
|
| 41 |
+
|
| 42 |
+
// 3. Trigger connection in background
|
| 43 |
+
go func() {
|
| 44 |
+
_ = cc.connectionService.Connect(context.Background(), accountID)
|
| 45 |
+
}()
|
| 46 |
+
|
| 47 |
+
// 4. Return Standardized Response
|
| 48 |
+
response := dto.ConnectResponse{
|
| 49 |
+
Message: entity.CONNECTION_INIT_SUCCESS,
|
| 50 |
+
AccountID: accountID,
|
| 51 |
+
Details: entity.QR_SCAN_INSTRUCTION,
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
utils.SendResponse[dto.ConnectResponse, any](ctx, dto.AuthResponse{}, response, nil)
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
func (cc *connectionController) GetStatus(ctx *gin.Context) {
|
| 58 |
+
userID, exists := ctx.Get("user_id")
|
| 59 |
+
if !exists {
|
| 60 |
+
utils.SendResponse[any, any](ctx, dto.AuthResponse{}, nil, http_error.UNAUTHORIZED)
|
| 61 |
+
return
|
| 62 |
+
}
|
| 63 |
+
accountID := userID.(uuid.UUID)
|
| 64 |
+
|
| 65 |
+
client, err := cc.connectionService.GetActiveClient(accountID)
|
| 66 |
+
if err != nil {
|
| 67 |
+
response := dto.ConnectionStatusResponse{
|
| 68 |
+
AccountID: accountID,
|
| 69 |
+
Status: entity.WHATSAPP_STATUS_DISCONNECTED,
|
| 70 |
+
}
|
| 71 |
+
utils.SendResponse[dto.ConnectionStatusResponse, any](ctx, dto.AuthResponse{}, response, nil)
|
| 72 |
+
return
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
status := entity.WHATSAPP_STATUS_DISCONNECTED
|
| 76 |
+
if client.IsConnected() {
|
| 77 |
+
status = entity.WHATSAPP_STATUS_CONNECTED
|
| 78 |
+
} else if client.IsLoggedIn() {
|
| 79 |
+
status = entity.WHATSAPP_STATUS_CONNECTING
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
response := dto.ConnectionStatusResponse{
|
| 83 |
+
AccountID: accountID,
|
| 84 |
+
Status: status,
|
| 85 |
+
JID: client.Store.ID.String(),
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
utils.SendResponse[dto.ConnectionStatusResponse, any](ctx, dto.AuthResponse{}, response, nil)
|
| 89 |
+
}
|
go.mod
CHANGED
|
@@ -3,18 +3,27 @@ module whatsapp-backend
|
|
| 3 |
go 1.24.5
|
| 4 |
|
| 5 |
require (
|
|
|
|
| 6 |
github.com/gin-gonic/gin v1.11.0
|
|
|
|
| 7 |
github.com/google/uuid v1.6.0
|
| 8 |
github.com/joho/godotenv v1.5.1
|
|
|
|
|
|
|
|
|
|
| 9 |
gorm.io/driver/postgres v1.6.0
|
| 10 |
gorm.io/gorm v1.31.1
|
| 11 |
)
|
| 12 |
|
| 13 |
require (
|
|
|
|
|
|
|
| 14 |
github.com/bytedance/gopkg v0.1.3 // indirect
|
| 15 |
github.com/bytedance/sonic v1.14.2 // indirect
|
| 16 |
github.com/bytedance/sonic/loader v0.4.0 // indirect
|
| 17 |
github.com/cloudwego/base64x v0.1.6 // indirect
|
|
|
|
|
|
|
| 18 |
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
| 19 |
github.com/gin-contrib/sse v1.1.0 // indirect
|
| 20 |
github.com/go-playground/locales v0.14.1 // indirect
|
|
@@ -31,20 +40,27 @@ require (
|
|
| 31 |
github.com/json-iterator/go v1.1.12 // indirect
|
| 32 |
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
| 33 |
github.com/leodido/go-urn v1.4.0 // indirect
|
|
|
|
| 34 |
github.com/mattn/go-isatty v0.0.20 // indirect
|
| 35 |
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
| 36 |
github.com/modern-go/reflect2 v1.0.2 // indirect
|
| 37 |
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
|
|
|
| 38 |
github.com/quic-go/qpack v0.6.0 // indirect
|
| 39 |
github.com/quic-go/quic-go v0.58.0 // indirect
|
|
|
|
| 40 |
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
| 41 |
github.com/ugorji/go/codec v1.3.1 // indirect
|
| 42 |
-
|
|
|
|
|
|
|
| 43 |
golang.org/x/arch v0.23.0 // indirect
|
| 44 |
-
golang.org/x/
|
| 45 |
golang.org/x/net v0.48.0 // indirect
|
| 46 |
golang.org/x/sync v0.19.0 // indirect
|
| 47 |
golang.org/x/sys v0.39.0 // indirect
|
|
|
|
| 48 |
golang.org/x/text v0.32.0 // indirect
|
| 49 |
google.golang.org/protobuf v1.36.11 // indirect
|
|
|
|
| 50 |
)
|
|
|
|
| 3 |
go 1.24.5
|
| 4 |
|
| 5 |
require (
|
| 6 |
+
github.com/gin-contrib/gzip v1.2.5
|
| 7 |
github.com/gin-gonic/gin v1.11.0
|
| 8 |
+
github.com/golang-jwt/jwt/v5 v5.3.0
|
| 9 |
github.com/google/uuid v1.6.0
|
| 10 |
github.com/joho/godotenv v1.5.1
|
| 11 |
+
github.com/mdp/qrterminal/v3 v3.2.1
|
| 12 |
+
go.mau.fi/whatsmeow v0.0.0-20251217143725-11cf47c62d32
|
| 13 |
+
golang.org/x/crypto v0.46.0
|
| 14 |
gorm.io/driver/postgres v1.6.0
|
| 15 |
gorm.io/gorm v1.31.1
|
| 16 |
)
|
| 17 |
|
| 18 |
require (
|
| 19 |
+
filippo.io/edwards25519 v1.1.0 // indirect
|
| 20 |
+
github.com/beeper/argo-go v1.1.2 // indirect
|
| 21 |
github.com/bytedance/gopkg v0.1.3 // indirect
|
| 22 |
github.com/bytedance/sonic v1.14.2 // indirect
|
| 23 |
github.com/bytedance/sonic/loader v0.4.0 // indirect
|
| 24 |
github.com/cloudwego/base64x v0.1.6 // indirect
|
| 25 |
+
github.com/coder/websocket v1.8.14 // indirect
|
| 26 |
+
github.com/elliotchance/orderedmap/v3 v3.1.0 // indirect
|
| 27 |
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
| 28 |
github.com/gin-contrib/sse v1.1.0 // indirect
|
| 29 |
github.com/go-playground/locales v0.14.1 // indirect
|
|
|
|
| 40 |
github.com/json-iterator/go v1.1.12 // indirect
|
| 41 |
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
| 42 |
github.com/leodido/go-urn v1.4.0 // indirect
|
| 43 |
+
github.com/mattn/go-colorable v0.1.14 // indirect
|
| 44 |
github.com/mattn/go-isatty v0.0.20 // indirect
|
| 45 |
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
| 46 |
github.com/modern-go/reflect2 v1.0.2 // indirect
|
| 47 |
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
| 48 |
+
github.com/petermattis/goid v0.0.0-20251121121749-a11dd1a45f9a // indirect
|
| 49 |
github.com/quic-go/qpack v0.6.0 // indirect
|
| 50 |
github.com/quic-go/quic-go v0.58.0 // indirect
|
| 51 |
+
github.com/rs/zerolog v1.34.0 // indirect
|
| 52 |
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
| 53 |
github.com/ugorji/go/codec v1.3.1 // indirect
|
| 54 |
+
github.com/vektah/gqlparser/v2 v2.5.27 // indirect
|
| 55 |
+
go.mau.fi/libsignal v0.2.1 // indirect
|
| 56 |
+
go.mau.fi/util v0.9.4 // indirect
|
| 57 |
golang.org/x/arch v0.23.0 // indirect
|
| 58 |
+
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 // indirect
|
| 59 |
golang.org/x/net v0.48.0 // indirect
|
| 60 |
golang.org/x/sync v0.19.0 // indirect
|
| 61 |
golang.org/x/sys v0.39.0 // indirect
|
| 62 |
+
golang.org/x/term v0.38.0 // indirect
|
| 63 |
golang.org/x/text v0.32.0 // indirect
|
| 64 |
google.golang.org/protobuf v1.36.11 // indirect
|
| 65 |
+
rsc.io/qr v0.2.0 // indirect
|
| 66 |
)
|
go.sum
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
| 2 |
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
| 3 |
github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE=
|
|
@@ -6,11 +16,18 @@ github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2N
|
|
| 6 |
github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
| 7 |
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
| 8 |
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
|
|
|
|
|
|
|
|
|
| 9 |
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
| 10 |
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
| 11 |
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
|
|
|
|
|
| 12 |
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
| 13 |
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
|
|
|
|
|
|
| 14 |
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
| 15 |
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
| 16 |
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
|
@@ -27,6 +44,9 @@ github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
|
| 27 |
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
| 28 |
github.com/goccy/go-yaml v1.19.1 h1:3rG3+v8pkhRqoQ/88NYNMHYVGYztCOCIZ7UQhu7H+NE=
|
| 29 |
github.com/goccy/go-yaml v1.19.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
|
|
|
|
|
|
|
|
|
| 30 |
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
| 31 |
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
| 32 |
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
|
@@ -52,8 +72,17 @@ github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzh
|
|
| 52 |
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
| 53 |
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
| 54 |
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
| 56 |
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
| 58 |
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
| 59 |
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
|
@@ -61,12 +90,20 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
|
|
| 61 |
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
| 62 |
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
| 63 |
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
|
|
|
|
|
|
|
|
|
| 64 |
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
| 65 |
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
| 66 |
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
| 67 |
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
| 68 |
github.com/quic-go/quic-go v0.58.0 h1:ggY2pvZaVdB9EyojxL1p+5mptkuHyX5MOSv4dgWF4Ug=
|
| 69 |
github.com/quic-go/quic-go v0.58.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
| 71 |
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
| 72 |
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
|
@@ -83,19 +120,33 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
|
|
| 83 |
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
| 84 |
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
| 85 |
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
| 87 |
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
| 88 |
golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
|
| 89 |
golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
| 90 |
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
| 91 |
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
|
|
|
|
|
|
| 92 |
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
| 93 |
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
| 94 |
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
| 95 |
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
|
|
|
| 96 |
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
|
|
| 97 |
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
| 98 |
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
|
|
|
|
|
|
| 99 |
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
| 100 |
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
| 101 |
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
|
@@ -108,3 +159,5 @@ gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
|
|
| 108 |
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
|
| 109 |
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
| 110 |
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
|
|
|
|
|
|
|
|
| 1 |
+
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
| 2 |
+
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
| 3 |
+
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
| 4 |
+
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
|
| 5 |
+
github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM=
|
| 6 |
+
github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU=
|
| 7 |
+
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
|
| 8 |
+
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
| 9 |
+
github.com/beeper/argo-go v1.1.2 h1:UQI2G8F+NLfGTOmTUI0254pGKx/HUU/etbUGTJv91Fs=
|
| 10 |
+
github.com/beeper/argo-go v1.1.2/go.mod h1:M+LJAnyowKVQ6Rdj6XYGEn+qcVFkb3R/MUpqkGR0hM4=
|
| 11 |
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
| 12 |
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
| 13 |
github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE=
|
|
|
|
| 16 |
github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
| 17 |
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
| 18 |
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
| 19 |
+
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
|
| 20 |
+
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
|
| 21 |
+
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
| 22 |
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
| 23 |
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
| 24 |
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
| 25 |
+
github.com/elliotchance/orderedmap/v3 v3.1.0 h1:j4DJ5ObEmMBt/lcwIecKcoRxIQUEnw0L804lXYDt/pg=
|
| 26 |
+
github.com/elliotchance/orderedmap/v3 v3.1.0/go.mod h1:G+Hc2RwaZvJMcS4JpGCOyViCnGeKf0bTYCGTO4uhjSo=
|
| 27 |
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
| 28 |
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
| 29 |
+
github.com/gin-contrib/gzip v1.2.5 h1:fIZs0S+l17pIu1P5XRJOo/YNqfIuPCrZZ3TWB7pjckI=
|
| 30 |
+
github.com/gin-contrib/gzip v1.2.5/go.mod h1:aomRgR7ftdZV3uWY0gW/m8rChfxau0n8YVvwlOHONzw=
|
| 31 |
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
| 32 |
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
| 33 |
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
|
|
|
| 44 |
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
| 45 |
github.com/goccy/go-yaml v1.19.1 h1:3rG3+v8pkhRqoQ/88NYNMHYVGYztCOCIZ7UQhu7H+NE=
|
| 46 |
github.com/goccy/go-yaml v1.19.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
| 47 |
+
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
| 48 |
+
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
| 49 |
+
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
| 50 |
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
| 51 |
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
| 52 |
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
|
|
|
| 72 |
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
| 73 |
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
| 74 |
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
| 75 |
+
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
| 76 |
+
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
| 77 |
+
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
| 78 |
+
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
| 79 |
+
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
| 80 |
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
| 81 |
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
| 82 |
+
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
|
| 83 |
+
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
| 84 |
+
github.com/mdp/qrterminal/v3 v3.2.1 h1:6+yQjiiOsSuXT5n9/m60E54vdgFsw0zhADHhHLrFet4=
|
| 85 |
+
github.com/mdp/qrterminal/v3 v3.2.1/go.mod h1:jOTmXvnBsMy5xqLniO0R++Jmjs2sTm9dFSuQ5kpz/SU=
|
| 86 |
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
| 87 |
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
| 88 |
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
|
|
|
| 90 |
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
| 91 |
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
| 92 |
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
| 93 |
+
github.com/petermattis/goid v0.0.0-20251121121749-a11dd1a45f9a h1:VweslR2akb/ARhXfqSfRbj1vpWwYXf3eeAUyw/ndms0=
|
| 94 |
+
github.com/petermattis/goid v0.0.0-20251121121749-a11dd1a45f9a/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
|
| 95 |
+
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
| 96 |
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
| 97 |
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
| 98 |
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
| 99 |
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
| 100 |
github.com/quic-go/quic-go v0.58.0 h1:ggY2pvZaVdB9EyojxL1p+5mptkuHyX5MOSv4dgWF4Ug=
|
| 101 |
github.com/quic-go/quic-go v0.58.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
| 102 |
+
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
| 103 |
+
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
| 104 |
+
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
| 105 |
+
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
| 106 |
+
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
| 107 |
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
| 108 |
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
| 109 |
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
|
|
|
| 120 |
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
| 121 |
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
| 122 |
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
| 123 |
+
github.com/vektah/gqlparser/v2 v2.5.27 h1:RHPD3JOplpk5mP5JGX8RKZkt2/Vwj/PZv0HxTdwFp0s=
|
| 124 |
+
github.com/vektah/gqlparser/v2 v2.5.27/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo=
|
| 125 |
+
go.mau.fi/libsignal v0.2.1 h1:vRZG4EzTn70XY6Oh/pVKrQGuMHBkAWlGRC22/85m9L0=
|
| 126 |
+
go.mau.fi/libsignal v0.2.1/go.mod h1:iVvjrHyfQqWajOUaMEsIfo3IqgVMrhWcPiiEzk7NgoU=
|
| 127 |
+
go.mau.fi/util v0.9.4 h1:gWdUff+K2rCynRPysXalqqQyr2ahkSWaestH6YhSpso=
|
| 128 |
+
go.mau.fi/util v0.9.4/go.mod h1:647nVfwUvuhlZFOnro3aRNPmRd2y3iDha9USb8aKSmM=
|
| 129 |
+
go.mau.fi/whatsmeow v0.0.0-20251217143725-11cf47c62d32 h1:NeE9eEYY4kEJVCfCXaAU27LgAPugPHRHJdC9IpXFPzI=
|
| 130 |
+
go.mau.fi/whatsmeow v0.0.0-20251217143725-11cf47c62d32/go.mod h1:S4OWR9+hTx+54+jRzl+NfRBXnGpPm5IRPyhXB7haSd0=
|
| 131 |
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
| 132 |
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
| 133 |
golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
|
| 134 |
golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
| 135 |
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
| 136 |
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
| 137 |
+
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 h1:MDfG8Cvcqlt9XXrmEiD4epKn7VJHZO84hejP9Jmp0MM=
|
| 138 |
+
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
|
| 139 |
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
| 140 |
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
| 141 |
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
| 142 |
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
| 143 |
+
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
| 144 |
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
| 145 |
+
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
| 146 |
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
| 147 |
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
| 148 |
+
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
|
| 149 |
+
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
|
| 150 |
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
| 151 |
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
| 152 |
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
|
|
|
| 159 |
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
|
| 160 |
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
| 161 |
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
| 162 |
+
rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
|
| 163 |
+
rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=
|
middleware/auth_middleware.go
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package middleware
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"strings"
|
| 5 |
+
"whatsapp-backend/config"
|
| 6 |
+
http_error "whatsapp-backend/models/error"
|
| 7 |
+
"whatsapp-backend/utils"
|
| 8 |
+
|
| 9 |
+
"github.com/gin-gonic/gin"
|
| 10 |
+
"github.com/golang-jwt/jwt/v5"
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
type AuthMiddleware interface {
|
| 14 |
+
RequireAuth() gin.HandlerFunc
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
type authMiddleware struct {
|
| 18 |
+
jwtConfig config.JWTConfig
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
func NewAuthMiddleware(jwtConfig config.JWTConfig) AuthMiddleware {
|
| 22 |
+
return &authMiddleware{jwtConfig: jwtConfig}
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
func (m *authMiddleware) RequireAuth() gin.HandlerFunc {
|
| 26 |
+
return func(ctx *gin.Context) {
|
| 27 |
+
authHeader := ctx.GetHeader("Authorization")
|
| 28 |
+
if authHeader == "" {
|
| 29 |
+
utils.SendResponse[any, any](ctx, nil, nil, http_error.UNAUTHORIZED)
|
| 30 |
+
ctx.Abort()
|
| 31 |
+
return
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
tokenString := strings.Replace(authHeader, "Bearer ", "", 1)
|
| 35 |
+
token, err := jwt.ParseWithClaims(tokenString, &utils.JWTClaims{}, func(token *jwt.Token) (interface{}, error) {
|
| 36 |
+
return []byte(m.jwtConfig.GetSecretKey()), nil
|
| 37 |
+
})
|
| 38 |
+
|
| 39 |
+
if err != nil || !token.Valid {
|
| 40 |
+
utils.SendResponse[any, any](ctx, nil, nil, http_error.INVALID_TOKEN)
|
| 41 |
+
ctx.Abort()
|
| 42 |
+
return
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
claims, ok := token.Claims.(*utils.JWTClaims)
|
| 46 |
+
if !ok {
|
| 47 |
+
utils.SendResponse[any, any](ctx, nil, nil, http_error.INVALID_TOKEN)
|
| 48 |
+
ctx.Abort()
|
| 49 |
+
return
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
// Set user info in context
|
| 53 |
+
ctx.Set("user_id", claims.UserID)
|
| 54 |
+
ctx.Set("username", claims.Username)
|
| 55 |
+
|
| 56 |
+
ctx.Next()
|
| 57 |
+
}
|
| 58 |
+
}
|
models/dto/auth_dto.go
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package dto
|
| 2 |
+
|
| 3 |
+
import "github.com/google/uuid"
|
| 4 |
+
|
| 5 |
+
type RegisterRequest struct {
|
| 6 |
+
Username string `json:"username" binding:"required,min=3"`
|
| 7 |
+
Password string `json:"password" binding:"required,min=6"`
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
type LoginRequest struct {
|
| 11 |
+
Username string `json:"username" binding:"required"`
|
| 12 |
+
Password string `json:"password" binding:"required"`
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
type AuthResponse struct {
|
| 16 |
+
Token string `json:"token"`
|
| 17 |
+
UserID uuid.UUID `json:"user_id"`
|
| 18 |
+
Username string `json:"username"`
|
| 19 |
+
}
|
models/dto/connection_dto.go
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package dto
|
| 2 |
+
|
| 3 |
+
import "github.com/google/uuid"
|
| 4 |
+
|
| 5 |
+
type ConnectRequest struct {
|
| 6 |
+
// Add fields here if needed in the future, e.g. force refresh
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
type ConnectResponse struct {
|
| 10 |
+
Message string `json:"message"`
|
| 11 |
+
AccountID uuid.UUID `json:"account_id"`
|
| 12 |
+
Details string `json:"details"`
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
type ConnectionStatusResponse struct {
|
| 16 |
+
AccountID uuid.UUID `json:"account_id"`
|
| 17 |
+
Status string `json:"status"`
|
| 18 |
+
JID string `json:"jid,omitempty"`
|
| 19 |
+
}
|
models/dto/http_response_dto.go
CHANGED
|
@@ -9,7 +9,7 @@ type SuccessResponse[TResponse any] struct {
|
|
| 9 |
|
| 10 |
type ErrorResponse struct {
|
| 11 |
Status string `json:"status"`
|
| 12 |
-
Error
|
| 13 |
Message any `json:"message"`
|
| 14 |
MetaData any `json:"meta_data"`
|
| 15 |
-
}
|
|
|
|
| 9 |
|
| 10 |
type ErrorResponse struct {
|
| 11 |
Status string `json:"status"`
|
| 12 |
+
Error any `json:"errors"`
|
| 13 |
Message any `json:"message"`
|
| 14 |
MetaData any `json:"meta_data"`
|
| 15 |
+
}
|
models/entity/constant.go
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package models
|
| 2 |
+
|
| 3 |
+
const (
|
| 4 |
+
CONNECTION_SUCCESS_MESSAGE = "Connection initialized. Please scan the QR code in your terminal."
|
| 5 |
+
CONNECTION_INIT_SUCCESS = "Connection process initiated successfully"
|
| 6 |
+
QR_SCAN_INSTRUCTION = "Check terminal for QR code scanning"
|
| 7 |
+
POSTGRES_DIALECT = "postgres"
|
| 8 |
+
WHATSAPP_STATUS_CONNECTED = "connected"
|
| 9 |
+
WHATSAPP_STATUS_DISCONNECTED = "disconnected"
|
| 10 |
+
WHATSAPP_STATUS_CONNECTING = "connecting"
|
| 11 |
+
)
|
models/entity/entity.go
CHANGED
|
@@ -1 +1,22 @@
|
|
| 1 |
package models
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
package models
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"time"
|
| 5 |
+
|
| 6 |
+
"github.com/google/uuid"
|
| 7 |
+
"gorm.io/gorm"
|
| 8 |
+
)
|
| 9 |
+
|
| 10 |
+
type WhatsAppAccount struct {
|
| 11 |
+
ID uuid.UUID `gorm:"primaryKey;type:uuid;default:gen_random_uuid()" json:"id"`
|
| 12 |
+
AccountName string `gorm:"size:100;not null" json:"account_name"`
|
| 13 |
+
JID string `gorm:"column:jid;size:255;uniqueIndex" json:"jid"`
|
| 14 |
+
IsActive bool `gorm:"default:false" json:"is_active"`
|
| 15 |
+
CreatedAt time.Time `json:"created_at"`
|
| 16 |
+
UpdatedAt time.Time `json:"updated_at"`
|
| 17 |
+
DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at"`
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
func (WhatsAppAccount) TableName() string {
|
| 21 |
+
return "whatsapp_accounts"
|
| 22 |
+
}
|
models/entity/user_entity.go
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package models
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"time"
|
| 5 |
+
|
| 6 |
+
"github.com/google/uuid"
|
| 7 |
+
"gorm.io/gorm"
|
| 8 |
+
)
|
| 9 |
+
|
| 10 |
+
type User struct {
|
| 11 |
+
ID uuid.UUID `gorm:"primaryKey;type:uuid;default:gen_random_uuid()" json:"id"`
|
| 12 |
+
Username string `gorm:"size:100;uniqueIndex;not null" json:"username"`
|
| 13 |
+
Password string `gorm:"not null" json:"-"` // Do not return password in JSON
|
| 14 |
+
CreatedAt time.Time `json:"created_at"`
|
| 15 |
+
UpdatedAt time.Time `json:"updated_at"`
|
| 16 |
+
DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at"`
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
func (User) TableName() string {
|
| 20 |
+
return "users"
|
| 21 |
+
}
|
models/error/error.go
CHANGED
|
@@ -24,4 +24,20 @@ var (
|
|
| 24 |
EVENT_FINISHED = errors.New("The event has ended, you were disallowed to do the exam!")
|
| 25 |
EVENT_NOT_STARTED = errors.New("Take it easy, event hasn't starting yet! you cannot do the exam!")
|
| 26 |
EXAMS_SUBMITTED = errors.New("You've submitted the exam, you were diasallowed to answer the question!")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
)
|
|
|
|
| 24 |
EVENT_FINISHED = errors.New("The event has ended, you were disallowed to do the exam!")
|
| 25 |
EVENT_NOT_STARTED = errors.New("Take it easy, event hasn't starting yet! you cannot do the exam!")
|
| 26 |
EXAMS_SUBMITTED = errors.New("You've submitted the exam, you were diasallowed to answer the question!")
|
| 27 |
+
|
| 28 |
+
// Auth Errors
|
| 29 |
+
ERR_USER_ALREADY_EXISTS = errors.New("User already exists")
|
| 30 |
+
ERR_TOKEN_GENERATION_FAILED = errors.New("Failed to generate token")
|
| 31 |
+
ERR_USER_NOT_FOUND = errors.New("User not found")
|
| 32 |
+
ERR_WRONG_PASSWORD = errors.New("Wrong password")
|
| 33 |
+
|
| 34 |
+
// WhatsApp Connection Errors
|
| 35 |
+
ERR_INVALID_ACCOUNT_ID = errors.New("Invalid account ID format")
|
| 36 |
+
ERR_CLIENT_INIT_FAILED = errors.New("Failed to initialize WhatsApp client")
|
| 37 |
+
ERR_CLIENT_CONNECT_FAILED = errors.New("Failed to connect to WhatsApp socket")
|
| 38 |
+
ERR_CLIENT_NOT_FOUND_FOR_ACC = errors.New("WhatsApp client not found for this account")
|
| 39 |
+
ERR_INVALID_JID = errors.New("Invalid JID format")
|
| 40 |
+
ERR_DB_CONNECTION_FAILED = errors.New("Failed to get database connection")
|
| 41 |
+
ERR_SQLSTORE_FAILED = errors.New("Failed to initialize SQL store")
|
| 42 |
+
ERR_DEVICE_STORE_FAILED = errors.New("Failed to retrieve device from store")
|
| 43 |
)
|
provider/controller_provider.go
CHANGED
|
@@ -1,11 +1,32 @@
|
|
| 1 |
package provider
|
| 2 |
|
|
|
|
|
|
|
| 3 |
type ControllerProvider interface {
|
|
|
|
|
|
|
| 4 |
}
|
| 5 |
|
| 6 |
type controllerProvider struct {
|
|
|
|
|
|
|
| 7 |
}
|
| 8 |
|
| 9 |
func NewControllerProvider(servicesProvider ServicesProvider) ControllerProvider {
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
}
|
|
|
|
| 1 |
package provider
|
| 2 |
|
| 3 |
+
import "whatsapp-backend/controllers"
|
| 4 |
+
|
| 5 |
type ControllerProvider interface {
|
| 6 |
+
ProvideConnectionController() controllers.ConnectionController
|
| 7 |
+
ProvideAuthController() controllers.AuthController
|
| 8 |
}
|
| 9 |
|
| 10 |
type controllerProvider struct {
|
| 11 |
+
connectionController controllers.ConnectionController
|
| 12 |
+
authController controllers.AuthController
|
| 13 |
}
|
| 14 |
|
| 15 |
func NewControllerProvider(servicesProvider ServicesProvider) ControllerProvider {
|
| 16 |
+
|
| 17 |
+
connectionController := controllers.NewConnectionController(servicesProvider.ProvideConnectionService())
|
| 18 |
+
authController := controllers.NewAuthController(servicesProvider.ProvideAuthService())
|
| 19 |
+
|
| 20 |
+
return &controllerProvider{
|
| 21 |
+
connectionController: connectionController,
|
| 22 |
+
authController: authController,
|
| 23 |
+
}
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
func (c *controllerProvider) ProvideConnectionController() controllers.ConnectionController {
|
| 27 |
+
return c.connectionController
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
func (c *controllerProvider) ProvideAuthController() controllers.AuthController {
|
| 31 |
+
return c.authController
|
| 32 |
}
|
provider/middleware_provider.go
CHANGED
|
@@ -1,11 +1,23 @@
|
|
| 1 |
package provider
|
| 2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
type MiddlewareProvider interface {
|
|
|
|
| 4 |
}
|
| 5 |
|
| 6 |
type middlewareProvider struct {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
}
|
| 8 |
|
| 9 |
-
func
|
| 10 |
-
return
|
| 11 |
}
|
|
|
|
| 1 |
package provider
|
| 2 |
|
| 3 |
+
import (
|
| 4 |
+
"whatsapp-backend/middleware"
|
| 5 |
+
)
|
| 6 |
+
|
| 7 |
type MiddlewareProvider interface {
|
| 8 |
+
ProvideAuthMiddleware() middleware.AuthMiddleware
|
| 9 |
}
|
| 10 |
|
| 11 |
type middlewareProvider struct {
|
| 12 |
+
authMiddleware middleware.AuthMiddleware
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
func NewMiddlewareProvider(servicesProvider ServicesProvider, configProvider ConfigProvider) MiddlewareProvider {
|
| 16 |
+
return &middlewareProvider{
|
| 17 |
+
authMiddleware: middleware.NewAuthMiddleware(configProvider.ProvideJWTConfig()),
|
| 18 |
+
}
|
| 19 |
}
|
| 20 |
|
| 21 |
+
func (m *middlewareProvider) ProvideAuthMiddleware() middleware.AuthMiddleware {
|
| 22 |
+
return m.authMiddleware
|
| 23 |
}
|
provider/provider.go
CHANGED
|
@@ -1,6 +1,8 @@
|
|
| 1 |
package provider
|
| 2 |
|
| 3 |
import (
|
|
|
|
|
|
|
| 4 |
"github.com/gin-gonic/gin"
|
| 5 |
)
|
| 6 |
|
|
@@ -27,8 +29,13 @@ func NewAppProvider() AppProvider {
|
|
| 27 |
repositoriesProvider := NewRepositoriesProvider(configProvider)
|
| 28 |
servicesProvider := NewServicesProvider(repositoriesProvider, configProvider)
|
| 29 |
controllerProvider := NewControllerProvider(servicesProvider)
|
| 30 |
-
middlewareProvider := NewMiddlewareProvider(servicesProvider)
|
| 31 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
|
| 33 |
return &appProvider{
|
| 34 |
ginRouter: ginRouter,
|
|
|
|
| 1 |
package provider
|
| 2 |
|
| 3 |
import (
|
| 4 |
+
entity "whatsapp-backend/models/entity"
|
| 5 |
+
|
| 6 |
"github.com/gin-gonic/gin"
|
| 7 |
)
|
| 8 |
|
|
|
|
| 29 |
repositoriesProvider := NewRepositoriesProvider(configProvider)
|
| 30 |
servicesProvider := NewServicesProvider(repositoriesProvider, configProvider)
|
| 31 |
controllerProvider := NewControllerProvider(servicesProvider)
|
| 32 |
+
middlewareProvider := NewMiddlewareProvider(servicesProvider, configProvider)
|
| 33 |
+
|
| 34 |
+
// Auto-Migrate Entities
|
| 35 |
+
_ = configProvider.ProvideDatabaseConfig().AutoMigrateAll(
|
| 36 |
+
&entity.WhatsAppAccount{},
|
| 37 |
+
&entity.User{},
|
| 38 |
+
)
|
| 39 |
|
| 40 |
return &appProvider{
|
| 41 |
ginRouter: ginRouter,
|
provider/repositories_provider.go
CHANGED
|
@@ -1,11 +1,32 @@
|
|
| 1 |
package provider
|
| 2 |
|
|
|
|
|
|
|
| 3 |
type RepositoriesProvider interface {
|
|
|
|
|
|
|
| 4 |
}
|
| 5 |
|
| 6 |
type repositoriesProvider struct {
|
|
|
|
|
|
|
| 7 |
}
|
| 8 |
|
| 9 |
func NewRepositoriesProvider(cfg ConfigProvider) RepositoriesProvider {
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
}
|
|
|
|
| 1 |
package provider
|
| 2 |
|
| 3 |
+
import "whatsapp-backend/repositories"
|
| 4 |
+
|
| 5 |
type RepositoriesProvider interface {
|
| 6 |
+
ProvideConnectionRepository() repositories.ConnectionRepository
|
| 7 |
+
ProvideAuthRepository() repositories.AuthRepository
|
| 8 |
}
|
| 9 |
|
| 10 |
type repositoriesProvider struct {
|
| 11 |
+
connectionRepository repositories.ConnectionRepository
|
| 12 |
+
authRepository repositories.AuthRepository
|
| 13 |
}
|
| 14 |
|
| 15 |
func NewRepositoriesProvider(cfg ConfigProvider) RepositoriesProvider {
|
| 16 |
+
db := cfg.ProvideDatabaseConfig().GetInstance()
|
| 17 |
+
connectionRepository, _ := repositories.NewConnectionRepository(db)
|
| 18 |
+
authRepository := repositories.NewAuthRepository(db)
|
| 19 |
+
|
| 20 |
+
return &repositoriesProvider{
|
| 21 |
+
connectionRepository: connectionRepository,
|
| 22 |
+
authRepository: authRepository,
|
| 23 |
+
}
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
func (rp *repositoriesProvider) ProvideConnectionRepository() repositories.ConnectionRepository {
|
| 27 |
+
return rp.connectionRepository
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
func (rp *repositoriesProvider) ProvideAuthRepository() repositories.AuthRepository {
|
| 31 |
+
return rp.authRepository
|
| 32 |
}
|
provider/services_provider.go
CHANGED
|
@@ -1,11 +1,31 @@
|
|
| 1 |
package provider
|
| 2 |
|
|
|
|
|
|
|
| 3 |
type ServicesProvider interface {
|
|
|
|
|
|
|
| 4 |
}
|
| 5 |
|
| 6 |
type servicesProvider struct {
|
|
|
|
|
|
|
| 7 |
}
|
| 8 |
|
| 9 |
func NewServicesProvider(repoProvider RepositoriesProvider, configProvider ConfigProvider) ServicesProvider {
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
}
|
|
|
|
| 1 |
package provider
|
| 2 |
|
| 3 |
+
import "whatsapp-backend/services"
|
| 4 |
+
|
| 5 |
type ServicesProvider interface {
|
| 6 |
+
ProvideConnectionService() services.ConnectionService
|
| 7 |
+
ProvideAuthService() services.AuthService
|
| 8 |
}
|
| 9 |
|
| 10 |
type servicesProvider struct {
|
| 11 |
+
connectionService services.ConnectionService
|
| 12 |
+
authService services.AuthService
|
| 13 |
}
|
| 14 |
|
| 15 |
func NewServicesProvider(repoProvider RepositoriesProvider, configProvider ConfigProvider) ServicesProvider {
|
| 16 |
+
connectionService := services.NewConnectionService(repoProvider.ProvideConnectionRepository())
|
| 17 |
+
authService := services.NewAuthService(repoProvider.ProvideAuthRepository(), configProvider.ProvideJWTConfig())
|
| 18 |
+
|
| 19 |
+
return &servicesProvider{
|
| 20 |
+
connectionService: connectionService,
|
| 21 |
+
authService: authService,
|
| 22 |
+
}
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
func (s *servicesProvider) ProvideConnectionService() services.ConnectionService {
|
| 26 |
+
return s.connectionService
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
func (s *servicesProvider) ProvideAuthService() services.AuthService {
|
| 30 |
+
return s.authService
|
| 31 |
}
|
repositories/auth_repository.go
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package repositories
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
entity "whatsapp-backend/models/entity"
|
| 5 |
+
|
| 6 |
+
"gorm.io/gorm"
|
| 7 |
+
)
|
| 8 |
+
|
| 9 |
+
type AuthRepository interface {
|
| 10 |
+
CreateUser(user *entity.User) error
|
| 11 |
+
FindUserByUsername(username string) (*entity.User, error)
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
type authRepository struct {
|
| 15 |
+
db *gorm.DB
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
func NewAuthRepository(db *gorm.DB) AuthRepository {
|
| 19 |
+
return &authRepository{db: db}
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
func (r *authRepository) CreateUser(user *entity.User) error {
|
| 23 |
+
return r.db.Create(user).Error
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
func (r *authRepository) FindUserByUsername(username string) (*entity.User, error) {
|
| 27 |
+
var user entity.User
|
| 28 |
+
if err := r.db.Where("username = ?", username).First(&user).Error; err != nil {
|
| 29 |
+
return nil, err
|
| 30 |
+
}
|
| 31 |
+
return &user, nil
|
| 32 |
+
}
|
repositories/connection_repositories.go
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package repositories
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"context"
|
| 5 |
+
"fmt"
|
| 6 |
+
|
| 7 |
+
entity "whatsapp-backend/models/entity"
|
| 8 |
+
http_error "whatsapp-backend/models/error"
|
| 9 |
+
|
| 10 |
+
"github.com/google/uuid"
|
| 11 |
+
"go.mau.fi/whatsmeow"
|
| 12 |
+
"go.mau.fi/whatsmeow/store"
|
| 13 |
+
"go.mau.fi/whatsmeow/store/sqlstore"
|
| 14 |
+
"go.mau.fi/whatsmeow/types"
|
| 15 |
+
waLog "go.mau.fi/whatsmeow/util/log"
|
| 16 |
+
"gorm.io/gorm"
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
type ConnectionRepository interface {
|
| 20 |
+
InitializeClient(ctx context.Context, accountID uuid.UUID) (*whatsmeow.Client, <-chan whatsmeow.QRChannelItem, error)
|
| 21 |
+
UpdateAccountStatus(accountID uuid.UUID, jid string, isActive bool) error
|
| 22 |
+
DeleteDevice(accountID uuid.UUID) error
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
type connectionRepository struct {
|
| 26 |
+
db *gorm.DB
|
| 27 |
+
container *sqlstore.Container
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
// NewConnectionRepository initializes the SQLStore ONCE during app startup
|
| 31 |
+
func NewConnectionRepository(db *gorm.DB) (ConnectionRepository, error) {
|
| 32 |
+
sqlDB, err := db.DB()
|
| 33 |
+
if err != nil {
|
| 34 |
+
return nil, http_error.ERR_DB_CONNECTION_FAILED
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
// whatsmeow requires its own logger for the store
|
| 38 |
+
dbLog := waLog.Stdout("Database", "INFO", true)
|
| 39 |
+
|
| 40 |
+
// We use "postgres" because your connection is Postgres.
|
| 41 |
+
// whatsmeow_device, whatsmeow_session, etc.
|
| 42 |
+
container := sqlstore.NewWithDB(sqlDB, entity.POSTGRES_DIALECT, dbLog)
|
| 43 |
+
if err := container.Upgrade(context.Background()); err != nil {
|
| 44 |
+
return nil, http_error.ERR_SQLSTORE_FAILED
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
return &connectionRepository{
|
| 48 |
+
db: db,
|
| 49 |
+
container: container,
|
| 50 |
+
}, nil
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
func (r *connectionRepository) InitializeClient(ctx context.Context, accountID uuid.UUID) (*whatsmeow.Client, <-chan whatsmeow.QRChannelItem, error) {
|
| 54 |
+
var acc entity.WhatsAppAccount
|
| 55 |
+
|
| 56 |
+
// 1. Find or Create your GORM entity
|
| 57 |
+
acc = entity.WhatsAppAccount{
|
| 58 |
+
ID: accountID,
|
| 59 |
+
AccountName: "Default Device",
|
| 60 |
+
}
|
| 61 |
+
if err := r.db.FirstOrCreate(&acc, entity.WhatsAppAccount{ID: accountID}).Error; err != nil {
|
| 62 |
+
return nil, nil, http_error.ERR_DB_CONNECTION_FAILED
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
var deviceStore *store.Device
|
| 66 |
+
var err error
|
| 67 |
+
|
| 68 |
+
// 2. Determine if we use an existing session (JID) or start a new one
|
| 69 |
+
if acc.JID != "" {
|
| 70 |
+
jid, parseErr := types.ParseJID(acc.JID)
|
| 71 |
+
if parseErr != nil {
|
| 72 |
+
return nil, nil, http_error.ERR_INVALID_JID
|
| 73 |
+
}
|
| 74 |
+
deviceStore, err = r.container.GetDevice(ctx, jid)
|
| 75 |
+
} else {
|
| 76 |
+
// No JID means this is a fresh account/new login
|
| 77 |
+
deviceStore = r.container.NewDevice()
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
if err != nil {
|
| 81 |
+
return nil, nil, http_error.ERR_DEVICE_STORE_FAILED
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
// 3. Create the whatsmeow client
|
| 85 |
+
clientLog := waLog.Stdout(fmt.Sprintf("Client-%s", accountID), "INFO", true)
|
| 86 |
+
client := whatsmeow.NewClient(deviceStore, clientLog)
|
| 87 |
+
|
| 88 |
+
// 4. Generate QR Channel if not logged in
|
| 89 |
+
var qrChan <-chan whatsmeow.QRChannelItem
|
| 90 |
+
if client.Store.ID == nil {
|
| 91 |
+
qrChan, _ = client.GetQRChannel(ctx)
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
return client, qrChan, nil
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
func (r *connectionRepository) UpdateAccountStatus(accountID uuid.UUID, jid string, isActive bool) error {
|
| 98 |
+
// Updates both the JID (bridge) and the active status in your GORM entity
|
| 99 |
+
return r.db.Model(&entity.WhatsAppAccount{ID: accountID}).Updates(map[string]interface{}{
|
| 100 |
+
"jid": jid,
|
| 101 |
+
"is_active": isActive,
|
| 102 |
+
}).Error
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
func (r *connectionRepository) DeleteDevice(accountID uuid.UUID) error {
|
| 106 |
+
var acc entity.WhatsAppAccount
|
| 107 |
+
if err := r.db.First(&acc, accountID).Error; err != nil {
|
| 108 |
+
return err
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
if acc.JID != "" {
|
| 112 |
+
jid, _ := types.ParseJID(acc.JID)
|
| 113 |
+
device, err := r.container.GetDevice(context.Background(), jid)
|
| 114 |
+
if err == nil && device != nil {
|
| 115 |
+
_ = device.Delete(context.Background()) // Pass context
|
| 116 |
+
}
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
// Also clear from DB
|
| 120 |
+
return r.UpdateAccountStatus(accountID, "", false)
|
| 121 |
+
}
|
repositories/repositories.go
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
package repositories
|
router/auth_router.go
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package router
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"whatsapp-backend/provider"
|
| 5 |
+
|
| 6 |
+
"github.com/gin-contrib/gzip"
|
| 7 |
+
"github.com/gin-gonic/gin"
|
| 8 |
+
)
|
| 9 |
+
|
| 10 |
+
func AuthRouter(router *gin.Engine, controller provider.ControllerProvider) {
|
| 11 |
+
authController := controller.ProvideAuthController()
|
| 12 |
+
routerGroup := router.Group("/api/auth")
|
| 13 |
+
routerGroup.Use(gzip.Gzip(gzip.DefaultCompression))
|
| 14 |
+
{
|
| 15 |
+
routerGroup.POST("/register", authController.Register)
|
| 16 |
+
routerGroup.POST("/login", authController.Login)
|
| 17 |
+
}
|
| 18 |
+
}
|
router/connection_router.go
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package router
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"whatsapp-backend/provider"
|
| 5 |
+
|
| 6 |
+
"github.com/gin-contrib/gzip"
|
| 7 |
+
"github.com/gin-gonic/gin"
|
| 8 |
+
)
|
| 9 |
+
|
| 10 |
+
func ConnectionRouter(router *gin.Engine, controller provider.ControllerProvider, middleware provider.MiddlewareProvider) {
|
| 11 |
+
connectionController := controller.ProvideConnectionController()
|
| 12 |
+
authMiddleware := middleware.ProvideAuthMiddleware()
|
| 13 |
+
|
| 14 |
+
routerGroup := router.Group("/api/whatsapp", authMiddleware.RequireAuth())
|
| 15 |
+
routerGroup.Use(gzip.Gzip(gzip.DefaultCompression))
|
| 16 |
+
{
|
| 17 |
+
routerGroup.POST("/connect", connectionController.Connect)
|
| 18 |
+
routerGroup.GET("/status", connectionController.GetStatus)
|
| 19 |
+
}
|
| 20 |
+
}
|
router/router.go
CHANGED
|
@@ -5,6 +5,13 @@ import (
|
|
| 5 |
)
|
| 6 |
|
| 7 |
func RunRouter(appProvider provider.AppProvider) {
|
| 8 |
-
router, config := appProvider.ProvideRouter(), appProvider.ProvideConfig()
|
| 9 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
}
|
|
|
|
| 5 |
)
|
| 6 |
|
| 7 |
func RunRouter(appProvider provider.AppProvider) {
|
| 8 |
+
router, controller, config, middleware := appProvider.ProvideRouter(), appProvider.ProvideControllers(), appProvider.ProvideConfig(), appProvider.ProvideMiddlewares()
|
| 9 |
+
|
| 10 |
+
ConnectionRouter(router, controller, middleware)
|
| 11 |
+
AuthRouter(router, controller)
|
| 12 |
+
|
| 13 |
+
err := router.Run(config.ProvideEnvConfig().GetTCPAddress())
|
| 14 |
+
if err != nil {
|
| 15 |
+
panic(err)
|
| 16 |
+
}
|
| 17 |
}
|
services/auth_service.go
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package services
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"whatsapp-backend/config"
|
| 5 |
+
"whatsapp-backend/models/dto"
|
| 6 |
+
entity "whatsapp-backend/models/entity"
|
| 7 |
+
http_error "whatsapp-backend/models/error"
|
| 8 |
+
"whatsapp-backend/repositories"
|
| 9 |
+
"whatsapp-backend/utils"
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
type AuthService interface {
|
| 13 |
+
Register(req dto.RegisterRequest) (*dto.AuthResponse, error)
|
| 14 |
+
Login(req dto.LoginRequest) (*dto.AuthResponse, error)
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
type authService struct {
|
| 18 |
+
authRepo repositories.AuthRepository
|
| 19 |
+
jwtConfig config.JWTConfig
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
func NewAuthService(authRepo repositories.AuthRepository, jwtConfig config.JWTConfig) AuthService {
|
| 23 |
+
return &authService{
|
| 24 |
+
authRepo: authRepo,
|
| 25 |
+
jwtConfig: jwtConfig,
|
| 26 |
+
}
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
func (s *authService) Register(req dto.RegisterRequest) (*dto.AuthResponse, error) {
|
| 30 |
+
// 1. Check if user exists
|
| 31 |
+
if _, err := s.authRepo.FindUserByUsername(req.Username); err == nil {
|
| 32 |
+
return nil, http_error.ERR_USER_ALREADY_EXISTS
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
// 2. Hash Password
|
| 36 |
+
hashedPassword, err := utils.HashPassword(req.Password)
|
| 37 |
+
if err != nil {
|
| 38 |
+
return nil, err
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
// 3. Create User
|
| 42 |
+
user := &entity.User{
|
| 43 |
+
Username: req.Username,
|
| 44 |
+
Password: hashedPassword,
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
if err := s.authRepo.CreateUser(user); err != nil {
|
| 48 |
+
return nil, err
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
// 4. Generate Token
|
| 52 |
+
token, err := utils.GenerateToken(user.ID, user.Username, s.jwtConfig)
|
| 53 |
+
if err != nil {
|
| 54 |
+
return nil, http_error.ERR_TOKEN_GENERATION_FAILED
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
return &dto.AuthResponse{
|
| 58 |
+
Token: token,
|
| 59 |
+
UserID: user.ID,
|
| 60 |
+
Username: user.Username,
|
| 61 |
+
}, nil
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
func (s *authService) Login(req dto.LoginRequest) (*dto.AuthResponse, error) {
|
| 65 |
+
// 1. Find User
|
| 66 |
+
user, err := s.authRepo.FindUserByUsername(req.Username)
|
| 67 |
+
if err != nil {
|
| 68 |
+
return nil, http_error.ERR_USER_NOT_FOUND
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
// 2. Check Password
|
| 72 |
+
if !utils.CheckPasswordHash(req.Password, user.Password) {
|
| 73 |
+
return nil, http_error.ERR_WRONG_PASSWORD
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
// 3. Generate Token
|
| 77 |
+
token, err := utils.GenerateToken(user.ID, user.Username, s.jwtConfig)
|
| 78 |
+
if err != nil {
|
| 79 |
+
return nil, http_error.ERR_TOKEN_GENERATION_FAILED
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
return &dto.AuthResponse{
|
| 83 |
+
Token: token,
|
| 84 |
+
UserID: user.ID,
|
| 85 |
+
Username: user.Username,
|
| 86 |
+
}, nil
|
| 87 |
+
}
|
services/connection_service.go
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package services
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"context"
|
| 5 |
+
"fmt"
|
| 6 |
+
"os"
|
| 7 |
+
"sync"
|
| 8 |
+
http_error "whatsapp-backend/models/error"
|
| 9 |
+
"whatsapp-backend/repositories"
|
| 10 |
+
|
| 11 |
+
"github.com/google/uuid"
|
| 12 |
+
"github.com/mdp/qrterminal/v3"
|
| 13 |
+
"go.mau.fi/whatsmeow"
|
| 14 |
+
"go.mau.fi/whatsmeow/types/events"
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
type ConnectionService interface {
|
| 18 |
+
Connect(ctx context.Context, accountID uuid.UUID) error
|
| 19 |
+
GetActiveClient(accountID uuid.UUID) (*whatsmeow.Client, error)
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
type connectionService struct {
|
| 23 |
+
connectionRepo repositories.ConnectionRepository
|
| 24 |
+
activeClients sync.Map
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
func NewConnectionService(connectionRepo repositories.ConnectionRepository) ConnectionService {
|
| 28 |
+
return &connectionService{
|
| 29 |
+
connectionRepo: connectionRepo,
|
| 30 |
+
}
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
func (s *connectionService) Connect(ctx context.Context, accountID uuid.UUID) error {
|
| 34 |
+
if existingClient, ok := s.activeClients.Load(accountID); ok {
|
| 35 |
+
client := existingClient.(*whatsmeow.Client)
|
| 36 |
+
if client.IsConnected() {
|
| 37 |
+
return nil // Already connected, no action needed
|
| 38 |
+
}
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
// 2. Fetch or Init Client from Repository
|
| 42 |
+
// We assume your Repo now has a method that takes accountID and returns (client, qrChan, err)
|
| 43 |
+
// by looking up the entity in the DB first.
|
| 44 |
+
client, qrChan, err := s.connectionRepo.InitializeClient(ctx, accountID)
|
| 45 |
+
if err != nil {
|
| 46 |
+
return http_error.ERR_CLIENT_INIT_FAILED
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
// 3. Register Event Handlers
|
| 50 |
+
// This is the "hook" that updates your database when the status changes
|
| 51 |
+
client.AddEventHandler(func(evt interface{}) {
|
| 52 |
+
s.handleWhatsAppEvent(accountID, client, evt)
|
| 53 |
+
})
|
| 54 |
+
|
| 55 |
+
// 4. Handle QR Channel in a background goroutine
|
| 56 |
+
if qrChan != nil {
|
| 57 |
+
go func() {
|
| 58 |
+
for evt := range qrChan {
|
| 59 |
+
if evt.Event == "code" {
|
| 60 |
+
fmt.Printf("\n[ACCOUNT %s] SCAN THIS QR CODE:\n", accountID)
|
| 61 |
+
qrterminal.GenerateHalfBlock(evt.Code, qrterminal.L, os.Stdout)
|
| 62 |
+
} else {
|
| 63 |
+
fmt.Printf("[ACCOUNT %s] Login Event: %s\n", accountID, evt.Event)
|
| 64 |
+
}
|
| 65 |
+
}
|
| 66 |
+
}()
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
// 5. Connect to WhatsApp Socket
|
| 70 |
+
if err := client.Connect(); err != nil {
|
| 71 |
+
_ = s.connectionRepo.DeleteDevice(accountID) // CLEANUP
|
| 72 |
+
return http_error.ERR_CLIENT_CONNECT_FAILED
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
// 6. Store in-memory
|
| 76 |
+
s.activeClients.Store(accountID, client)
|
| 77 |
+
|
| 78 |
+
return nil
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
func (s *connectionService) GetActiveClient(accountID uuid.UUID) (*whatsmeow.Client, error) {
|
| 82 |
+
val, ok := s.activeClients.Load(accountID)
|
| 83 |
+
if !ok {
|
| 84 |
+
return nil, http_error.ERR_CLIENT_NOT_FOUND_FOR_ACC
|
| 85 |
+
}
|
| 86 |
+
return val.(*whatsmeow.Client), nil
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
// handleWhatsAppEvent coordinates between the live socket and your database
|
| 90 |
+
func (s *connectionService) handleWhatsAppEvent(accountID uuid.UUID, client *whatsmeow.Client, evt interface{}) {
|
| 91 |
+
switch evt.(type) {
|
| 92 |
+
case *events.Connected:
|
| 93 |
+
// When the socket confirms connection, update the JID in your DB entity
|
| 94 |
+
if client.Store.ID != nil {
|
| 95 |
+
_ = s.connectionRepo.UpdateAccountStatus(accountID, client.Store.ID.String(), true)
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
case *events.LoggedOut:
|
| 99 |
+
// Cleanup when the user unlinks the device from their phone
|
| 100 |
+
s.activeClients.Delete(accountID)
|
| 101 |
+
_ = s.connectionRepo.UpdateAccountStatus(accountID, "", false)
|
| 102 |
+
|
| 103 |
+
case *events.Disconnected:
|
| 104 |
+
// Temporary network loss - whatsmeow handles auto-reconnect,
|
| 105 |
+
// but we might want to log this.
|
| 106 |
+
fmt.Printf("Account %s disconnected from WhatsApp\n", accountID)
|
| 107 |
+
}
|
| 108 |
+
}
|
services/service.go
CHANGED
|
@@ -1 +1 @@
|
|
| 1 |
-
package services
|
|
|
|
| 1 |
+
package services
|
utils/jwt_util.go
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package utils
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"time"
|
| 5 |
+
"whatsapp-backend/config"
|
| 6 |
+
"github.com/golang-jwt/jwt/v5"
|
| 7 |
+
"github.com/google/uuid"
|
| 8 |
+
)
|
| 9 |
+
|
| 10 |
+
type JWTClaims struct {
|
| 11 |
+
UserID uuid.UUID `json:"user_id"`
|
| 12 |
+
Username string `json:"username"`
|
| 13 |
+
jwt.RegisteredClaims
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
func GenerateToken(userID uuid.UUID, username string, jwtConfig config.JWTConfig) (string, error) {
|
| 17 |
+
claims := JWTClaims{
|
| 18 |
+
UserID: userID,
|
| 19 |
+
Username: username,
|
| 20 |
+
RegisteredClaims: jwt.RegisteredClaims{
|
| 21 |
+
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)), // 1 day expiration
|
| 22 |
+
IssuedAt: jwt.NewNumericDate(time.Now()),
|
| 23 |
+
},
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
| 27 |
+
return token.SignedString([]byte(jwtConfig.GetSecretKey()))
|
| 28 |
+
}
|
utils/password_util.go
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package utils
|
| 2 |
+
|
| 3 |
+
import "golang.org/x/crypto/bcrypt"
|
| 4 |
+
|
| 5 |
+
func HashPassword(password string) (string, error) {
|
| 6 |
+
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
| 7 |
+
return string(bytes), err
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
func CheckPasswordHash(password, hash string) bool {
|
| 11 |
+
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
| 12 |
+
return err == nil
|
| 13 |
+
}
|
utils/response_util.go
CHANGED
|
@@ -6,6 +6,7 @@ import (
|
|
| 6 |
http_error "whatsapp-backend/models/error"
|
| 7 |
|
| 8 |
"github.com/gin-gonic/gin"
|
|
|
|
| 9 |
"gorm.io/gorm"
|
| 10 |
)
|
| 11 |
|
|
@@ -60,7 +61,43 @@ func ResponseFAILED[TMetaData any](c *gin.Context, metaData TMetaData, err error
|
|
| 60 |
})
|
| 61 |
return
|
| 62 |
} else {
|
| 63 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
Status: "error",
|
| 65 |
Error: err,
|
| 66 |
Message: err.Error(),
|
|
@@ -85,3 +122,17 @@ func SendResponse[Tdata any, TMetaData any](c *gin.Context, metaData TMetaData,
|
|
| 85 |
}
|
| 86 |
|
| 87 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
http_error "whatsapp-backend/models/error"
|
| 7 |
|
| 8 |
"github.com/gin-gonic/gin"
|
| 9 |
+
"github.com/go-playground/validator/v10"
|
| 10 |
"gorm.io/gorm"
|
| 11 |
)
|
| 12 |
|
|
|
|
| 61 |
})
|
| 62 |
return
|
| 63 |
} else {
|
| 64 |
+
// Check for Validation Errors
|
| 65 |
+
var validationErrors validator.ValidationErrors
|
| 66 |
+
if errors.As(err, &validationErrors) {
|
| 67 |
+
friendlyErrors := make([]string, len(validationErrors))
|
| 68 |
+
for i, fieldError := range validationErrors {
|
| 69 |
+
friendlyErrors[i] = msgForTag(fieldError)
|
| 70 |
+
}
|
| 71 |
+
c.JSON(400, dto.ErrorResponse{
|
| 72 |
+
Status: "error",
|
| 73 |
+
Error: friendlyErrors,
|
| 74 |
+
Message: "Validation Failed",
|
| 75 |
+
MetaData: nil,
|
| 76 |
+
})
|
| 77 |
+
return
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
// Check for Auth Errors
|
| 81 |
+
if errors.Is(err, http_error.ERR_USER_NOT_FOUND) || errors.Is(err, http_error.ERR_WRONG_PASSWORD) {
|
| 82 |
+
c.JSON(401, dto.ErrorResponse{
|
| 83 |
+
Status: "error",
|
| 84 |
+
Error: err,
|
| 85 |
+
Message: err.Error(),
|
| 86 |
+
MetaData: metaData,
|
| 87 |
+
})
|
| 88 |
+
return
|
| 89 |
+
} else if errors.Is(err, http_error.ERR_USER_ALREADY_EXISTS) {
|
| 90 |
+
c.JSON(409, dto.ErrorResponse{
|
| 91 |
+
Status: "error",
|
| 92 |
+
Error: err,
|
| 93 |
+
Message: err.Error(),
|
| 94 |
+
MetaData: metaData,
|
| 95 |
+
})
|
| 96 |
+
return
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
// Default to 500
|
| 100 |
+
c.JSON(500, dto.ErrorResponse{
|
| 101 |
Status: "error",
|
| 102 |
Error: err,
|
| 103 |
Message: err.Error(),
|
|
|
|
| 122 |
}
|
| 123 |
|
| 124 |
}
|
| 125 |
+
|
| 126 |
+
func msgForTag(fe validator.FieldError) string {
|
| 127 |
+
switch fe.Tag() {
|
| 128 |
+
case "required":
|
| 129 |
+
return "This field is required"
|
| 130 |
+
case "email":
|
| 131 |
+
return "Invalid email format"
|
| 132 |
+
case "min":
|
| 133 |
+
return fe.Field() + " must be longer than " + fe.Param() + " characters"
|
| 134 |
+
case "max":
|
| 135 |
+
return fe.Field() + " must be shorter than " + fe.Param() + " characters"
|
| 136 |
+
}
|
| 137 |
+
return fe.Error() // Default error message
|
| 138 |
+
}
|