feat: add JWT authentication middleware and login endpoint
This commit is contained in:
@@ -37,37 +37,40 @@ Start `whatsapp-bridge` -> then run `whatsapp-mcp-server` in your preferred mode
|
||||
The first time you run it, you will be prompted to scan a QR code. Scan the QR code with your WhatsApp mobile app to authenticate.
|
||||
After approximately 20 days, you will might need to re-authenticate.
|
||||
|
||||
3. **Connect to the MCP server**
|
||||
```bash
|
||||
cd whatsapp-mcp-server
|
||||
go build -o whatsapp-mcp
|
||||
```
|
||||
Copy the below json with the appropriate {{PATH}} values:
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"whatsapp-mcp": {
|
||||
"command": "{{PROJECT_BASE_PATH}}/whatsapp-mcp-server/whatsapp-mcp"
|
||||
}
|
||||
},
|
||||
"preferences": {
|
||||
"sidebarMode": "chat",
|
||||
"coworkScheduledTasksEnabled": false
|
||||
}
|
||||
}
|
||||
```
|
||||
3. **Connect to the MCP server**
|
||||
```bash
|
||||
cd whatsapp-mcp-server
|
||||
go build -o whatsapp-mcp
|
||||
```
|
||||
Copy the below json with the appropriate {{PATH}} values:
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"whatsapp-mcp": {
|
||||
"command": "{{PROJECT_BASE_PATH}}/whatsapp-mcp-server/whatsapp-mcp",
|
||||
"env": {
|
||||
"WHATSAPP_API_SECRET": "c3VwZXItbG9uZy1yYW5kb20tc3RyaW5nLW1pbmltdW0tb2YtNjQtY2hhcmFjdGVycy15b3UtbmVlZC10by1wYXN0ZS1oZXJl"
|
||||
}
|
||||
}
|
||||
},
|
||||
"preferences": {
|
||||
"sidebarMode": "chat",
|
||||
"coworkScheduledTasksEnabled": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For **Claude**, save this as `claude_desktop_config.json` in your Claude Desktop configuration directory at:
|
||||
For **Claude**, save this as `claude_desktop_config.json` in your Claude Desktop configuration directory at:
|
||||
|
||||
```
|
||||
~/Library/Application Support/Claude/claude_desktop_config.json
|
||||
```
|
||||
```
|
||||
~/Library/Application Support/Claude/claude_desktop_config.json
|
||||
```
|
||||
|
||||
For **Cursor**, save this as `mcp.json` in your Cursor configuration directory at:
|
||||
For **Cursor**, save this as `mcp.json` in your Cursor configuration directory at:
|
||||
|
||||
```
|
||||
~/.cursor/mcp.json
|
||||
```
|
||||
```
|
||||
~/.cursor/mcp.json
|
||||
```
|
||||
|
||||
### Windows Compatibility
|
||||
|
||||
|
||||
Generated
+4
-1
@@ -1,6 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$/../../../.." vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
|
||||
</component>
|
||||
<component name="VcsProjectSettings">
|
||||
<option name="detectVcsMappingsAutomatically" value="false" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,78 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
"whatsapp-bridge/config"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
type Claims struct {
|
||||
Service string `json:"service"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
func LoginHandler(cfg *config.Config) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
auth := r.Header.Get("Authorization")
|
||||
|
||||
if auth != fmt.Sprintf("Bearer %s", cfg.APIKey) {
|
||||
fmt.Println("Invalid API key")
|
||||
http.Error(w, "Invalid credentials", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
claims := Claims{
|
||||
Service: "mcp-server",
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(45 * time.Minute)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
},
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
signed, err := token.SignedString(cfg.JWTSecret)
|
||||
if err != nil {
|
||||
fmt.Println("Failed to sign token:", err)
|
||||
http.Error(w, "Failed to generate token", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]string{"token": signed})
|
||||
}
|
||||
}
|
||||
|
||||
// JwtAuthMiddleware Protect normal API endpoints
|
||||
func JwtAuthMiddleware(cfg *config.Config, next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
auth := r.Header.Get("Authorization")
|
||||
if !strings.HasPrefix(auth, "Bearer ") {
|
||||
http.Error(w, "Missing or invalid Authorization header", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
tokenStr := strings.TrimPrefix(auth, "Bearer ")
|
||||
|
||||
token, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
return cfg.JWTSecret, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
fmt.Println("Parse error:", err)
|
||||
http.Error(w, "Invalid or expired token", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
if !token.Valid {
|
||||
fmt.Println("Token is NOT valid")
|
||||
http.Error(w, "Invalid or expired token", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
type dbConfig struct {
|
||||
User string
|
||||
Pass string
|
||||
Host string
|
||||
Port string
|
||||
IsPostgres bool
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
DB dbConfig
|
||||
JWTSecret []byte
|
||||
APIKey string
|
||||
}
|
||||
|
||||
func LoadConfig() (*Config, error) {
|
||||
user, ok := os.LookupEnv("POSTGRES_USER")
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing POSTGRES_USER")
|
||||
}
|
||||
pass, ok := os.LookupEnv("POSTGRES_PASS")
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing POSTGRES_PASS")
|
||||
}
|
||||
host, ok := os.LookupEnv("POSTGRES_HOST")
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing POSTGRES_HOST")
|
||||
}
|
||||
port, ok := os.LookupEnv("POSTGRES_PORT")
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing POSTGRES_PORT")
|
||||
}
|
||||
|
||||
jwtSecret, ok := os.LookupEnv("JWT_SECRET")
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing JWT_SECRET")
|
||||
}
|
||||
apiKey, ok := os.LookupEnv("API_KEY")
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing API_KEY")
|
||||
}
|
||||
|
||||
isPostgres := os.Getenv("IS_POSTGRES") == "true"
|
||||
|
||||
return &Config{
|
||||
DB: dbConfig{
|
||||
User: user,
|
||||
Pass: pass,
|
||||
Host: host,
|
||||
Port: port,
|
||||
IsPostgres: isPostgres,
|
||||
},
|
||||
JWTSecret: []byte(jwtSecret),
|
||||
APIKey: apiKey,
|
||||
}, nil
|
||||
}
|
||||
@@ -11,12 +11,16 @@ services:
|
||||
IS_POSTGRES: "true"
|
||||
POSTGRES_USER: "test"
|
||||
POSTGRES_PASS: "test"
|
||||
POSTGRES_HOST: "192.168.1.100"
|
||||
POSTGRES_HOST: "postgres"
|
||||
POSTGRES_PORT: "5432"
|
||||
API_KEY: "c3VwZXItbG9uZy1yYW5kb20tc3RyaW5nLW1pbmltdW0tb2YtNjQtY2hhcmFjdGVycy15b3UtbmVlZC10by1wYXN0ZS1oZXJl"
|
||||
JWT_SECRET: "YW5vdGhlci1zdXBlci1sb25nLXJhbmRvbS1zdHJpbmctbWluaW11bS1vZi02NC1jaGFyYWN0ZXJzLXlvdS1uZWVkLXRvLXBhc3RlLWhlcmU="
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- media:/project/store
|
||||
depends_on:
|
||||
- postgres
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
@@ -25,6 +29,19 @@ services:
|
||||
reservations:
|
||||
cpus: "0.5"
|
||||
memory: "64M"
|
||||
postgres:
|
||||
image: postgres:16
|
||||
container_name: postgres
|
||||
restart: always
|
||||
environment:
|
||||
POSTGRES_USER: "test"
|
||||
POSTGRES_PASSWORD: "test"
|
||||
POSTGRES_DB: "testdb"
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
|
||||
volumes:
|
||||
media:
|
||||
pgdata:
|
||||
|
||||
@@ -3,10 +3,11 @@ module whatsapp-bridge
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||
github.com/lib/pq v1.11.2
|
||||
github.com/mattn/go-sqlite3 v1.14.34
|
||||
github.com/mdp/qrterminal v1.0.1
|
||||
go.mau.fi/whatsmeow v0.0.0-20260219150138-7ae702b1eed4
|
||||
go.mau.fi/whatsmeow v0.0.0-20260305215846-fc65416c22c4
|
||||
google.golang.org/protobuf v1.36.11
|
||||
)
|
||||
|
||||
@@ -16,7 +17,6 @@ require (
|
||||
github.com/coder/websocket v1.8.14 // indirect
|
||||
github.com/elliotchance/orderedmap/v3 v3.1.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741 // indirect
|
||||
@@ -26,8 +26,8 @@ require (
|
||||
go.mau.fi/util v0.9.6 // indirect
|
||||
golang.org/x/crypto v0.48.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect
|
||||
golang.org/x/net v0.50.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/net v0.51.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
rsc.io/qr v0.2.0 // indirect
|
||||
)
|
||||
|
||||
+18
-52
@@ -1,9 +1,11 @@
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
||||
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
|
||||
github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM=
|
||||
github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU=
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
||||
github.com/beeper/argo-go v1.1.2 h1:UQI2G8F+NLfGTOmTUI0254pGKx/HUU/etbUGTJv91Fs=
|
||||
github.com/beeper/argo-go v1.1.2/go.mod h1:M+LJAnyowKVQ6Rdj6XYGEn+qcVFkb3R/MUpqkGR0hM4=
|
||||
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
|
||||
@@ -14,14 +16,12 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
||||
github.com/elliotchance/orderedmap/v3 v3.1.0 h1:j4DJ5ObEmMBt/lcwIecKcoRxIQUEnw0L804lXYDt/pg=
|
||||
github.com/elliotchance/orderedmap/v3 v3.1.0/go.mod h1:G+Hc2RwaZvJMcS4JpGCOyViCnGeKf0bTYCGTO4uhjSo=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lib/pq v1.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs=
|
||||
github.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
@@ -31,18 +31,10 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.29 h1:1O6nRLJKvsi1H2Sj0Hzdfojwt8GiGKm+LOfLaBFaouQ=
|
||||
github.com/mattn/go-sqlite3 v1.14.29/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
|
||||
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk=
|
||||
github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mdp/qrterminal v1.0.1 h1:07+fzVDlPuBlXS8tB0ktTAyf+Lp1j2+2zK3fBOL5b7c=
|
||||
github.com/mdp/qrterminal v1.0.1/go.mod h1:Z33WhxQe9B6CdW37HaVqcRKzP+kByF3q/qLxOGe12xQ=
|
||||
github.com/petermattis/goid v0.0.0-20250508124226-395b08cebbdb h1:3PrKuO92dUTMrQ9dx0YNejC6U/Si6jqKmyQ9vWjwqR4=
|
||||
github.com/petermattis/goid v0.0.0-20250508124226-395b08cebbdb/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
|
||||
github.com/petermattis/goid v0.0.0-20251121121749-a11dd1a45f9a h1:VweslR2akb/ARhXfqSfRbj1vpWwYXf3eeAUyw/ndms0=
|
||||
github.com/petermattis/goid v0.0.0-20251121121749-a11dd1a45f9a/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
|
||||
github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741 h1:KPpdlQLZcHfTMQRi6bFQ7ogNO0ltFT4PmtwTLW4W+14=
|
||||
github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
@@ -51,63 +43,37 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/vektah/gqlparser/v2 v2.5.31 h1:YhWGA1mfTjID7qJhd1+Vxhpk5HTgydrGU9IgkWBTJ7k=
|
||||
github.com/vektah/gqlparser/v2 v2.5.31/go.mod h1:c1I28gSOVNzlfc4WuDlqU7voQnsqI6OG2amkBAFmgts=
|
||||
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
||||
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/vektah/gqlparser/v2 v2.5.32 h1:k9QPJd4sEDTL+qB4ncPLflqTJ3MmjB9SrVzJrawpFSc=
|
||||
github.com/vektah/gqlparser/v2 v2.5.32/go.mod h1:c1I28gSOVNzlfc4WuDlqU7voQnsqI6OG2amkBAFmgts=
|
||||
go.mau.fi/libsignal v0.2.0 h1:oRXj3OHhEJq51BFEM8/50UZblmWiTYH93hsNTPcbk90=
|
||||
go.mau.fi/libsignal v0.2.0/go.mod h1:tvjoDsMejgT38CXTXwqaYu8itBiY8O2Mb6biWvZBb9k=
|
||||
go.mau.fi/libsignal v0.2.1 h1:vRZG4EzTn70XY6Oh/pVKrQGuMHBkAWlGRC22/85m9L0=
|
||||
go.mau.fi/libsignal v0.2.1/go.mod h1:iVvjrHyfQqWajOUaMEsIfo3IqgVMrhWcPiiEzk7NgoU=
|
||||
go.mau.fi/util v0.8.8 h1:OnuEEc/sIJFhnq4kFggiImUpcmnmL/xpvQMRu5Fiy5c=
|
||||
go.mau.fi/util v0.8.8/go.mod h1:Y/kS3loxTEhy8Vill513EtPXr+CRDdae+Xj2BXXMy/c=
|
||||
go.mau.fi/util v0.9.4 h1:gWdUff+K2rCynRPysXalqqQyr2ahkSWaestH6YhSpso=
|
||||
go.mau.fi/util v0.9.4/go.mod h1:647nVfwUvuhlZFOnro3aRNPmRd2y3iDha9USb8aKSmM=
|
||||
go.mau.fi/util v0.9.6 h1:2nsvxm49KhI3wrFltr0+wSUBlnQ4CMtykuELjpIU+ts=
|
||||
go.mau.fi/util v0.9.6/go.mod h1:sIJpRH7Iy5Ad1SBuxQoatxtIeErgzxCtjd/2hCMkYMI=
|
||||
go.mau.fi/whatsmeow v0.0.0-20250723174453-937d77661333 h1:wZnqbIembr69yyUeWWEjxNxeofhYg3PDEx1BRRvHdWM=
|
||||
go.mau.fi/whatsmeow v0.0.0-20250723174453-937d77661333/go.mod h1:ltDTXUgOAT7LcFKp11H+5S7UY7+xHBMGzNJcv3dLHGk=
|
||||
go.mau.fi/whatsmeow v0.0.0-20251217143725-11cf47c62d32 h1:NeE9eEYY4kEJVCfCXaAU27LgAPugPHRHJdC9IpXFPzI=
|
||||
go.mau.fi/whatsmeow v0.0.0-20251217143725-11cf47c62d32/go.mod h1:S4OWR9+hTx+54+jRzl+NfRBXnGpPm5IRPyhXB7haSd0=
|
||||
go.mau.fi/whatsmeow v0.0.0-20260219150138-7ae702b1eed4 h1:hsmlwsM+VqfF70cpdZEeIUKer2XWCQmQPK0u0tHy3ZQ=
|
||||
go.mau.fi/whatsmeow v0.0.0-20260219150138-7ae702b1eed4/go.mod h1:mXCRFyPEPn4jqWz6Afirn8vY7DpHCPnlKq6I2cWwFHM=
|
||||
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
|
||||
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
go.mau.fi/whatsmeow v0.0.0-20260305215846-fc65416c22c4 h1:FGA3NtCVNeCJ+C+KBg1pODsrfxC/trM3RHFWIeY7y4c=
|
||||
go.mau.fi/whatsmeow v0.0.0-20260305215846-fc65416c22c4/go.mod h1:mXCRFyPEPn4jqWz6Afirn8vY7DpHCPnlKq6I2cWwFHM=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc h1:TS73t7x3KarrNd5qAipmspBDS1rkMcgVG/fS1aRb4Rc=
|
||||
golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc=
|
||||
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 h1:MDfG8Cvcqlt9XXrmEiD4epKn7VJHZO84hejP9Jmp0MM=
|
||||
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
|
||||
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0=
|
||||
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
|
||||
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
||||
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
||||
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
||||
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
||||
+34
-63
@@ -20,6 +20,8 @@ import (
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
"whatsapp-bridge/auth"
|
||||
"whatsapp-bridge/config"
|
||||
|
||||
"go.mau.fi/whatsmeow/proto/waCompanionReg"
|
||||
"go.mau.fi/whatsmeow/socket"
|
||||
@@ -100,57 +102,18 @@ type MessageStore struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
type dbConfig struct {
|
||||
User string
|
||||
Pass string
|
||||
Host string
|
||||
Port string
|
||||
IsPostgres bool
|
||||
}
|
||||
|
||||
var isPostgres = false
|
||||
|
||||
func getEnv() (*dbConfig, error) {
|
||||
user, ok := os.LookupEnv("POSTGRES_USER")
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing POSTGRES_USER")
|
||||
}
|
||||
pass, ok := os.LookupEnv("POSTGRES_PASS")
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing POSTGRES_PASS")
|
||||
}
|
||||
host, ok := os.LookupEnv("POSTGRES_HOST")
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing POSTGRES_HOST")
|
||||
}
|
||||
port, ok := os.LookupEnv("POSTGRES_PORT")
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing POSTGRES_PORT")
|
||||
}
|
||||
IsPostgres, ok := os.LookupEnv("IS_POSTGRES")
|
||||
if !ok {
|
||||
IsPostgres = "false"
|
||||
}
|
||||
|
||||
isPostgres = IsPostgres == "true"
|
||||
|
||||
return &dbConfig{
|
||||
User: user,
|
||||
Pass: pass,
|
||||
Host: host,
|
||||
Port: port,
|
||||
IsPostgres: isPostgres,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func openDatabase(dbName string) (*sql.DB, error) {
|
||||
if val, ok := os.LookupEnv("IS_POSTGRES"); ok && strings.ToLower(val) == "true" {
|
||||
config, err := getEnv()
|
||||
cfg, err := config.LoadConfig()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("missing environment variable")
|
||||
}
|
||||
isPostgres = cfg.DB.IsPostgres
|
||||
|
||||
connStr := fmt.Sprintf("postgresql://%s:%s@%s:%s/%s?sslmode=disable", config.User, config.Pass, config.Host, config.Port, dbName)
|
||||
connStr := fmt.Sprintf("postgresql://%s:%s@%s:%s/%s?sslmode=disable",
|
||||
cfg.DB.User, cfg.DB.Pass, cfg.DB.Host, cfg.DB.Port, dbName)
|
||||
log.Println("Connecting to postgres")
|
||||
return sql.Open("postgres", connStr)
|
||||
}
|
||||
@@ -861,8 +824,11 @@ func extractDirectPathFromURL(url string) string {
|
||||
}
|
||||
|
||||
// Start a REST API server to expose the WhatsApp client functionality
|
||||
func startRESTServer(client *whatsmeow.Client, messageStore *MessageStore, port int) {
|
||||
http.HandleFunc("/api/send", func(w http.ResponseWriter, r *http.Request) {
|
||||
func startRESTServer(client *whatsmeow.Client, messageStore *MessageStore, port int, cfg *config.Config) {
|
||||
apiMux := http.NewServeMux()
|
||||
|
||||
// Send message
|
||||
apiMux.HandleFunc("/send", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
@@ -901,7 +867,7 @@ func startRESTServer(client *whatsmeow.Client, messageStore *MessageStore, port
|
||||
})
|
||||
|
||||
// Handler for downloading media
|
||||
http.HandleFunc("/api/download", func(w http.ResponseWriter, r *http.Request) {
|
||||
apiMux.HandleFunc("/download", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
@@ -945,7 +911,7 @@ func startRESTServer(client *whatsmeow.Client, messageStore *MessageStore, port
|
||||
})
|
||||
|
||||
// List recent chats
|
||||
http.HandleFunc("/api/chats", func(w http.ResponseWriter, r *http.Request) {
|
||||
apiMux.HandleFunc("/chats", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
@@ -981,13 +947,13 @@ func startRESTServer(client *whatsmeow.Client, messageStore *MessageStore, port
|
||||
})
|
||||
|
||||
// Get single chat
|
||||
http.HandleFunc("/api/chats/", func(w http.ResponseWriter, r *http.Request) {
|
||||
apiMux.HandleFunc("/chats/", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
jid := strings.TrimPrefix(r.URL.Path, "/api/chats/")
|
||||
jid := strings.TrimPrefix(r.URL.Path, "/chats/")
|
||||
if jid == "" {
|
||||
http.Error(w, "Missing chat JID", http.StatusBadRequest)
|
||||
return
|
||||
@@ -1009,7 +975,7 @@ func startRESTServer(client *whatsmeow.Client, messageStore *MessageStore, port
|
||||
})
|
||||
|
||||
// List messages (very flexible)
|
||||
http.HandleFunc("/api/messages", func(w http.ResponseWriter, r *http.Request) {
|
||||
apiMux.HandleFunc("/messages", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
@@ -1065,13 +1031,13 @@ func startRESTServer(client *whatsmeow.Client, messageStore *MessageStore, port
|
||||
})
|
||||
|
||||
// Get message + context
|
||||
http.HandleFunc("/api/messages/context/", func(w http.ResponseWriter, r *http.Request) {
|
||||
apiMux.HandleFunc("/messages/context/", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
messageID := strings.TrimPrefix(r.URL.Path, "/api/messages/context/")
|
||||
messageID := strings.TrimPrefix(r.URL.Path, "/messages/context/")
|
||||
if messageID == "" {
|
||||
http.Error(w, "Missing message ID", http.StatusBadRequest)
|
||||
return
|
||||
@@ -1096,7 +1062,7 @@ func startRESTServer(client *whatsmeow.Client, messageStore *MessageStore, port
|
||||
})
|
||||
|
||||
// Search contacts
|
||||
http.HandleFunc("/api/contacts/search", func(w http.ResponseWriter, r *http.Request) {
|
||||
apiMux.HandleFunc("/contacts/search", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
@@ -1119,15 +1085,15 @@ func startRESTServer(client *whatsmeow.Client, messageStore *MessageStore, port
|
||||
})
|
||||
})
|
||||
|
||||
// GET /api/contacts/:phone/chat
|
||||
// GET /api/direct-contacts/:phone/chat
|
||||
// Find the 1:1 (direct) chat for a given phone number
|
||||
http.HandleFunc("/api/direct-contacts/", func(w http.ResponseWriter, r *http.Request) {
|
||||
apiMux.HandleFunc("/direct-contacts/", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
path := strings.TrimPrefix(r.URL.Path, "/api/direct-contacts/")
|
||||
path := strings.TrimPrefix(r.URL.Path, "/direct-contacts/")
|
||||
parts := strings.Split(path, "/")
|
||||
if len(parts) < 2 || parts[1] != "chat" {
|
||||
http.Error(w, "Invalid path. Use /api/direct-contacts/{phone}/chat", http.StatusBadRequest)
|
||||
@@ -1159,13 +1125,13 @@ func startRESTServer(client *whatsmeow.Client, messageStore *MessageStore, port
|
||||
|
||||
// GET /api/contacts/:jid/chats
|
||||
// List all chats where this contact (by JID) appears as sender or in group
|
||||
http.HandleFunc("/api/contacts/", func(w http.ResponseWriter, r *http.Request) {
|
||||
apiMux.HandleFunc("/contacts/", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
path := strings.TrimPrefix(r.URL.Path, "/api/contacts/")
|
||||
path := strings.TrimPrefix(r.URL.Path, "/contacts/")
|
||||
parts := strings.Split(path, "/")
|
||||
if len(parts) < 2 || parts[1] != "chats" {
|
||||
// Skip if not this endpoint (previous handler already took /chat)
|
||||
@@ -1199,6 +1165,11 @@ func startRESTServer(client *whatsmeow.Client, messageStore *MessageStore, port
|
||||
})
|
||||
})
|
||||
|
||||
// Authentication
|
||||
protected := auth.JwtAuthMiddleware(cfg, apiMux)
|
||||
http.Handle("/api/", http.StripPrefix("/api", protected))
|
||||
http.Handle("/auth/login", auth.LoginHandler(cfg))
|
||||
|
||||
serverAddr := fmt.Sprintf(":%d", port)
|
||||
fmt.Printf("Starting REST API server on %s...\n", serverAddr)
|
||||
|
||||
@@ -2362,7 +2333,7 @@ func main() {
|
||||
return
|
||||
}
|
||||
|
||||
config, err := getEnv()
|
||||
cfg, err := config.LoadConfig()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -2370,10 +2341,10 @@ func main() {
|
||||
dialect := "sqlite3"
|
||||
connStr := "file:store/whatsapp.db?_foreign_keys=on"
|
||||
|
||||
if config.IsPostgres {
|
||||
if cfg.DB.IsPostgres {
|
||||
dialect = "postgres"
|
||||
connStr = fmt.Sprintf("postgresql://%s:%s@%s:%s/%s?sslmode=disable", config.User,
|
||||
config.Pass, config.Host, config.Port, "whatsapp")
|
||||
connStr = fmt.Sprintf("postgresql://%s:%s@%s:%s/%s?sslmode=disable", cfg.DB.User,
|
||||
cfg.DB.Pass, cfg.DB.Host, cfg.DB.Port, "whatsapp")
|
||||
}
|
||||
|
||||
container, err := sqlstore.New(context.Background(), dialect, connStr, dbLog)
|
||||
@@ -2477,7 +2448,7 @@ func main() {
|
||||
|
||||
fmt.Println("\n✓ Connected to WhatsApp! Type 'help' for commands.")
|
||||
|
||||
startRESTServer(client, messageStore, 8080)
|
||||
startRESTServer(client, messageStore, 8080, cfg)
|
||||
|
||||
exitChan := make(chan os.Signal, 1)
|
||||
signal.Notify(exitChan, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
Reference in New Issue
Block a user