feat: add JWT authentication middleware and login endpoint

This commit is contained in:
iamatulsingh
2026-03-08 21:01:04 +01:00
parent b5ac108719
commit cc6f466c72
8 changed files with 248 additions and 148 deletions
+30 -27
View File
@@ -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
+4 -1
View File
@@ -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>
+78
View File
@@ -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)
})
}
+62
View File
@@ -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
}
+18 -1
View File
@@ -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:
+4 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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)