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 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
- return os.Getenv("HOST_ADDRESS") + ":" + os.Getenv("HOST_PORT")
 
 
 
 
 
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
- go.uber.org/mock v0.6.0 // indirect
 
 
43
  golang.org/x/arch v0.23.0 // indirect
44
- golang.org/x/crypto v0.46.0 // indirect
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 error `json:"errors"`
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
- return &controllerProvider{}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 NewMiddlewareProvider(servicesProvider ServicesProvider) MiddlewareProvider {
10
- return &middlewareProvider{}
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
- // configProvider.ProvideDatabaseConfig().AutoMigrateAll()
 
 
 
 
 
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
- return &repositoriesProvider{}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- return &servicesProvider{}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- router.Run(config.ProvideEnvConfig().GetTCPAddress())
 
 
 
 
 
 
 
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
- c.JSON(405, dto.ErrorResponse{
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ }