Skip to content

Commit 68f93af

Browse files
author
Nicolás Gudiño
committed
fix: repair proxy migration and pairing-code flow
Add an idempotent webhook proxy migration with a collision-free ID and improve database and migration diagnostics. Fix duplicate session connection requests and pairing-code display errors in the dashboard.
1 parent d2e07cc commit 68f93af

5 files changed

Lines changed: 121 additions & 43 deletions

File tree

db.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88

99
"github.com/jmoiron/sqlx"
1010
_ "github.com/lib/pq"
11+
"github.com/rs/zerolog/log"
1112
_ "modernc.org/sqlite"
1213
)
1314

@@ -85,6 +86,20 @@ func initializePostgres(config DatabaseConfig) (*sqlx.DB, error) {
8586
return nil, fmt.Errorf("failed to ping postgres database: %w", err)
8687
}
8788

89+
var databaseName, schemaName string
90+
if err := db.QueryRow("SELECT current_database(), current_schema()").Scan(&databaseName, &schemaName); err != nil {
91+
return nil, fmt.Errorf("failed to identify postgres database: %w", err)
92+
}
93+
log.Info().
94+
Str("driver", "postgres").
95+
Str("host", config.Host).
96+
Str("port", config.Port).
97+
Str("database", databaseName).
98+
Str("schema", schemaName).
99+
Str("user", config.User).
100+
Str("sslmode", config.SSLMode).
101+
Msg("Database connection established")
102+
88103
return db, nil
89104
}
90105

handlers.go

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,8 @@ func (s *server) Connect() http.HandlerFunc {
277277
if clientManager.GetWhatsmeowClient(txtid) != nil {
278278
isConnected := clientManager.GetWhatsmeowClient(txtid).IsConnected()
279279
if isConnected == true {
280-
s.Respond(w, r, http.StatusInternalServerError, errors.New("already connected"))
280+
log.Warn().Str("user_id", txtid).Msg("Connect request rejected because client is already connected")
281+
s.Respond(w, r, http.StatusConflict, errors.New("already connected"))
281282
return
282283
}
283284
}
@@ -729,6 +730,8 @@ func (s *server) PairPhone() http.HandlerFunc {
729730
return
730731
}
731732

733+
log.Info().Str("user_id", txtid).Msg("Requesting WhatsApp phone pairing code")
734+
732735
isLoggedIn := clientManager.GetWhatsmeowClient(txtid).IsLoggedIn()
733736
if isLoggedIn {
734737
log.Error().Msg(fmt.Sprintf("%s", "already paired"))
@@ -738,7 +741,7 @@ func (s *server) PairPhone() http.HandlerFunc {
738741

739742
linkingCode, err := clientManager.GetWhatsmeowClient(txtid).PairPhone(context.Background(), t.Phone, true, whatsmeow.PairClientChrome, "Chrome (Linux)")
740743
if err != nil {
741-
log.Error().Msg(fmt.Sprintf("%s", err))
744+
log.Error().Err(err).Str("user_id", txtid).Msg("Failed to request WhatsApp phone pairing code")
742745
s.Respond(w, r, http.StatusBadRequest, err)
743746
return
744747
}
@@ -5460,6 +5463,11 @@ func (s *server) ListUsers() http.HandlerFunc {
54605463

54615464
rows, err := s.db.Queryx(query, args...)
54625465
if err != nil {
5466+
log.Error().
5467+
Err(err).
5468+
Str("driver", s.db.DriverName()).
5469+
Bool("single_user", hasID).
5470+
Msg("Failed to query users for admin request")
54635471
s.Respond(w, r, http.StatusInternalServerError, errors.New("problem accessing DB"))
54645472
return
54655473
}
@@ -5472,7 +5480,10 @@ func (s *server) ListUsers() http.HandlerFunc {
54725480
var user usersStruct
54735481
err := rows.StructScan(&user)
54745482
if err != nil {
5475-
log.Error().Str("error", fmt.Sprintf("%v", err)).Msg("admin DB error")
5483+
log.Error().
5484+
Err(err).
5485+
Str("driver", s.db.DriverName()).
5486+
Msg("Failed to scan user for admin request")
54765487
s.Respond(w, r, http.StatusInternalServerError, errors.New("problem accessing DB"))
54775488
return
54785489
}
@@ -5539,6 +5550,10 @@ func (s *server) ListUsers() http.HandlerFunc {
55395550
}
55405551
// Check for any error that occurred during iteration
55415552
if err := rows.Err(); err != nil {
5553+
log.Error().
5554+
Err(err).
5555+
Str("driver", s.db.DriverName()).
5556+
Msg("Failed while iterating users for admin request")
55425557
s.Respond(w, r, http.StatusInternalServerError, errors.New("problem accessing DB"))
55435558
return
55445559
}

main.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -268,8 +268,8 @@ func main() {
268268
}
269269

270270
log.Info().
271-
Bool("use_proxy", *globalWebhookUseProxy).
272-
Msg("Webhook Proxy Configured")
271+
Bool("use_proxy_when_configured", *globalWebhookUseProxy).
272+
Msg("Webhook proxy routing policy configured")
273273

274274
log.Info().
275275
Bool("enabled", *webhookRetryEnabled).

migrations.go

Lines changed: 59 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"strings"
88

99
"github.com/jmoiron/sqlx"
10+
"github.com/rs/zerolog/log"
1011
)
1112

1213
type Migration struct {
@@ -80,6 +81,11 @@ var migrations = []Migration{
8081
Name: "add_webhook_use_proxy",
8182
UpSQL: addWebhookUseProxySQL,
8283
},
84+
{
85+
ID: 12,
86+
Name: "repair_webhook_use_proxy",
87+
UpSQL: repairWebhookUseProxySQL,
88+
},
8389
}
8490

8591
const changeIDToStringSQL = `
@@ -236,16 +242,19 @@ END $$;
236242

237243
const addWebhookUseProxySQL = `
238244
-- PostgreSQL version
239-
DO $$
240-
BEGIN
241-
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'users' AND column_name = 'webhook_use_proxy') THEN
242-
ALTER TABLE users ADD COLUMN webhook_use_proxy BOOLEAN DEFAULT TRUE;
243-
END IF;
244-
END $$;
245+
ALTER TABLE users ADD COLUMN IF NOT EXISTS webhook_use_proxy BOOLEAN DEFAULT TRUE;
245246
246247
-- SQLite version (handled in code)
247248
`
248249

250+
// Migration 10 collided with migrations from another development branch and
251+
// also used an information_schema lookup that was not scoped to the active
252+
// schema. Keep this as a separate, globally unused migration ID so databases
253+
// that already recorded a different migration 10 or 11 are repaired.
254+
const repairWebhookUseProxySQL = `
255+
ALTER TABLE users ADD COLUMN IF NOT EXISTS webhook_use_proxy BOOLEAN DEFAULT TRUE;
256+
`
257+
249258
// GenerateRandomID creates a random string ID
250259
func GenerateRandomID() (string, error) {
251260
bytes := make([]byte, 16) // 128 bits
@@ -269,11 +278,51 @@ func initializeSchema(db *sqlx.DB) error {
269278
}
270279

271280
// Apply missing migrations
281+
sourceMigrations := make(map[int]string, len(migrations))
282+
for _, migration := range migrations {
283+
sourceMigrations[migration.ID] = migration.Name
284+
if appliedName, ok := applied[migration.ID]; ok && appliedName != migration.Name {
285+
log.Warn().
286+
Int("migration_id", migration.ID).
287+
Str("database_name", appliedName).
288+
Str("binary_name", migration.Name).
289+
Msg("Database migration ID has a different name in this binary")
290+
}
291+
}
292+
for id, appliedName := range applied {
293+
if _, ok := sourceMigrations[id]; !ok {
294+
log.Warn().
295+
Int("migration_id", id).
296+
Str("database_name", appliedName).
297+
Msg("Database contains a migration unknown to this binary")
298+
}
299+
}
300+
301+
pending := 0
302+
for _, migration := range migrations {
303+
if _, ok := applied[migration.ID]; !ok {
304+
pending++
305+
}
306+
}
307+
log.Info().
308+
Str("driver", db.DriverName()).
309+
Int("applied", len(applied)).
310+
Int("pending", pending).
311+
Msg("Database migration status")
312+
272313
for _, migration := range migrations {
273314
if _, ok := applied[migration.ID]; !ok {
315+
log.Info().
316+
Int("migration_id", migration.ID).
317+
Str("migration_name", migration.Name).
318+
Msg("Applying database migration")
274319
if err := applyMigration(db, migration); err != nil {
275320
return fmt.Errorf("failed to apply migration %d: %w", migration.ID, err)
276321
}
322+
log.Info().
323+
Int("migration_id", migration.ID).
324+
Str("migration_name", migration.Name).
325+
Msg("Database migration applied")
277326
}
278327
}
279328

@@ -322,8 +371,8 @@ func createMigrationsTable(db *sqlx.DB) error {
322371
return nil
323372
}
324373

325-
func getAppliedMigrations(db *sqlx.DB) (map[int]struct{}, error) {
326-
applied := make(map[int]struct{})
374+
func getAppliedMigrations(db *sqlx.DB) (map[int]string, error) {
375+
applied := make(map[int]string)
327376
var rows []struct {
328377
ID int `db:"id"`
329378
Name string `db:"name"`
@@ -335,7 +384,7 @@ func getAppliedMigrations(db *sqlx.DB) (map[int]struct{}, error) {
335384
}
336385

337386
for _, row := range rows {
338-
applied[row.ID] = struct{}{}
387+
applied[row.ID] = row.Name
339388
}
340389

341390
return applied, nil
@@ -473,7 +522,7 @@ func applyMigration(db *sqlx.DB, migration Migration) error {
473522
} else {
474523
_, err = tx.Exec(migration.UpSQL)
475524
}
476-
} else if migration.ID == 10 {
525+
} else if migration.ID == 10 || migration.ID == 12 {
477526
if db.DriverName() == "sqlite" {
478527
err = addColumnIfNotExistsSQLite(tx, "users", "webhook_use_proxy", "BOOLEAN DEFAULT 1")
479528
} else {

static/dashboard/js/app.js

Lines changed: 27 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -189,31 +189,31 @@ document.addEventListener('DOMContentLoaded', function() {
189189
return false;
190190
});
191191

192-
document.getElementById('pairphoneinput').addEventListener('keypress', function(e) {
193-
if (e.key === 'Enter') {
194-
const phone = e.currentTarget.value.trim();
195-
if (phone) {
196-
connect().then((data) => {
197-
if(data.success==true) {
198-
pairPhone(phone)
199-
.then((data) => {
200-
document.getElementById('pairHelp').classList.add('hidden');;
201-
// Success case
202-
if (data.success && data.data && data.data.LinkingCode) {
203-
document.getElementById('pairInfo').innerHTML = `Your link code is: ${data.data.LinkingCode}`;
204-
scanInterval = setInterval(checkStatus, 1000);
205-
} else {
206-
document.getElementById('pairInfo').innerHTML = "Problem getting pairing code";
207-
}
208-
})
209-
.catch((error) => {
210-
// Error case
211-
document.getElementById('pairInfo').innerHTML = "Problem getting pairing code";
212-
console.error('Pairing error:', error);
213-
});
214-
}
215-
});
192+
document.getElementById('pairphoneinput').addEventListener('keypress', async function(e) {
193+
if (e.key !== 'Enter') return;
194+
195+
e.preventDefault();
196+
const input = e.currentTarget;
197+
const phone = input.value.trim();
198+
if (!phone || input.disabled) return;
199+
200+
// This modal is shown only after /session/connect has established the
201+
// WhatsApp socket. Calling connect() again here made the pairing flow fail
202+
// with "already connected" before /session/pairphone was ever requested.
203+
input.disabled = true;
204+
try {
205+
const data = await pairPhone(phone);
206+
document.getElementById('pairHelp').classList.add('hidden');
207+
if (data.success && data.data && data.data.LinkingCode) {
208+
document.getElementById('pairInfo').textContent = `Your link code is: ${data.data.LinkingCode}`;
209+
} else {
210+
document.getElementById('pairInfo').textContent = `Problem getting pairing code: ${data.error || 'unknown error'}`;
216211
}
212+
} catch (error) {
213+
document.getElementById('pairInfo').textContent = "Problem getting pairing code: request failed";
214+
console.error('Pairing error:', error);
215+
} finally {
216+
input.disabled = false;
217217
}
218218
});
219219

@@ -1067,12 +1067,12 @@ async function pairPhone(phone) {
10671067
const myHeaders = new Headers();
10681068
myHeaders.append('token', token);
10691069
myHeaders.append('Content-Type', 'application/json');
1070-
res = await fetch(baseUrl + "/session/pairphone", {
1070+
const res = await fetch(baseUrl + "/session/pairphone", {
10711071
method: "POST",
10721072
headers: myHeaders,
10731073
body: JSON.stringify({Phone: phone})
10741074
});
1075-
data = await res.json();
1075+
const data = await res.json();
10761076
return data;
10771077
}
10781078

@@ -1167,7 +1167,6 @@ function init() {
11671167

11681168
// Starting
11691169
let notoken=0;
1170-
let scanInterval;
11711170
let token = getLocalStorageItem('token');
11721171
let admintoken = getLocalStorageItem('admintoken');
11731172
let isAdminLogin = getLocalStorageItem('isAdmin');
@@ -1513,7 +1512,7 @@ function populateInstances(instances) {
15131512
<div class="extra content">
15141513
<button class="ui primary positive button dashboard-button ${instance.connected === true ? 'hidden' : ''}" id="button-connect-${instance.id}" onclick="connect('${instance.token}')">Connect</button>
15151514
<button class="ui primary negative button dashboard-button ${instance.connected === true ? '' : 'hidden'}" id="button-logout-${instance.id}" onclick="logout('${instance.token}')">Logout</button>
1516-
<button class="ui primary positive button dashboard-button ${instance.connected === true && instance.loggedIn === false ? '' : 'hidden'} id="button-logout-${instance.id}" onclick="modalPairPhone()">Login with Pairing Code</button>
1515+
<button class="ui primary positive button dashboard-button ${instance.connected === true && instance.loggedIn === false ? '' : 'hidden'}" id="button-pair-${instance.id}" onclick="modalPairPhone()">Login with Pairing Code</button>
15171516
</div>
15181517
</div>
15191518
`;

0 commit comments

Comments
 (0)