Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
445e87a
Add Google Messages cookie reauthentication
Phloraxx Jul 27, 2026
b91606f
Stop auth retry loops and report real connector readiness
Phloraxx Jul 27, 2026
dc33e16
Expose dashboard-only Google reauthentication endpoint
Phloraxx Jul 27, 2026
02b5562
Test Google Messages reauthentication state handling
Phloraxx Jul 27, 2026
790323a
Protect Google reauthentication behind dashboard auth
Phloraxx Jul 27, 2026
f9e6d7a
Add Google login refresh flow to connector settings
Phloraxx Jul 27, 2026
3f1f562
Format Google reauth API test
Phloraxx Jul 27, 2026
f8cb1c9
Harden Google reauth and force fresh relay auth
Phloraxx Jul 27, 2026
0704aa4
Separate pairing identity validation from Google cookie validity
Phloraxx Jul 27, 2026
6a5c709
Format hardened Google reauth flow
Phloraxx Jul 27, 2026
dd1e315
Handle forced reauth connect failures without retry storms
Phloraxx Jul 27, 2026
b4faecd
Keep reauth retries bounded by auth state
Phloraxx Jul 27, 2026
1fad495
Temporarily apply reviewed reconnect ownership fix
Phloraxx Jul 27, 2026
49b1f9a
Fix Google reconnect ownership race
github-actions[bot] Jul 27, 2026
ebe21e1
Remove temporary PR19 patch workflow
Phloraxx Jul 27, 2026
ec03ea2
Cover stale reconnect timer ownership
Phloraxx Jul 27, 2026
b025694
Temporarily apply reviewed client event ownership fix
Phloraxx Jul 27, 2026
65a71b5
Ignore events from retired Google Messages clients
github-actions[bot] Jul 27, 2026
12c2c2a
Remove temporary PR19 event patch workflow
Phloraxx Jul 27, 2026
deb1ab4
Cover retired Google Messages client events
Phloraxx Jul 27, 2026
aa49700
Make retired-client regression inspect manager state directly
Phloraxx Jul 27, 2026
bcca626
Gate events while retiring Google Messages clients
Phloraxx Jul 27, 2026
f979cba
Drain retired client events before Google reauth replacement
Phloraxx Jul 27, 2026
fb59e91
Temporarily wire reviewed client event gate
Phloraxx Jul 27, 2026
2aba1a0
Drain retired Google Messages client events
github-actions[bot] Jul 27, 2026
93b0950
Remove temporary PR19 event gate workflow
Phloraxx Jul 27, 2026
701819a
Temporarily apply reviewed session lifecycle fixes
Phloraxx Jul 27, 2026
da5bd02
Preserve Google pairing across auth expiry
github-actions[bot] Jul 27, 2026
4fe85c5
Remove temporary self-writing PR workflow
Phloraxx Jul 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ func (a *API) Register(app core.App) {
e.Router.GET("/api/dashboard", a.dashboard)
e.Router.GET("/api/connector/gmessages/status", a.gmessagesStatus)
e.Router.POST("/api/connector/gmessages/pair/google", a.gmessagesGooglePair).Bind(apis.BodyLimit(maxGMessagesPairBytes))
e.Router.POST("/api/connector/gmessages/reauth/google", a.gmessagesGoogleReauth).Bind(apis.BodyLimit(maxGMessagesPairBytes))
e.Router.POST("/api/connector/gmessages/pair/qr", a.gmessagesPair)
e.Router.POST("/api/connector/gmessages/pair/qr/refresh", a.gmessagesPairRefresh)
// Backward-compatible QR aliases from the first PayGate rebuild.
Expand Down Expand Up @@ -289,6 +290,23 @@ func (a *API) gmessagesGooglePair(e *core.RequestEvent) error {
})
}

func (a *API) gmessagesGoogleReauth(e *core.RequestEvent) error {
if !a.dashboardAuth(e) {
return e.UnauthorizedError("dashboard authentication is required", nil)
}
if a.GMessages == nil {
return e.BadRequestError("Google Messages connector is unavailable", nil)
}
var body googleMessagesPairBody
if err := decodeJSON(e, &body); err != nil {
return e.BadRequestError("invalid JSON body", err)
}
if err := a.GMessages.ReauthenticateGoogle(strings.TrimSpace(body.CookieData)); err != nil {
return e.BadRequestError(err.Error(), nil)
}
return e.JSON(http.StatusOK, a.GMessages.Status())
}

func (a *API) gmessagesPair(e *core.RequestEvent) error {
if !a.dashboardAuth(e) {
return e.UnauthorizedError("dashboard authentication is required", nil)
Expand Down
38 changes: 38 additions & 0 deletions internal/api/gmessages_reauth_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package api

import (
"net/http"
"strings"
"testing"

"github.com/pocketbase/pocketbase/tests"
)

func TestGoogleMessagesReauthEndpointRequiresDashboardAuth(t *testing.T) {
scenarios := []tests.ApiScenario{
{
Name: "reauth requires dashboard auth", Method: http.MethodPost,
URL: "/api/connector/gmessages/reauth/google",
Headers: map[string]string{"Content-Type": "application/json"},
Body: strings.NewReader(`{"cookieData":"SID=missing-rest"}`),
TestAppFactory: func(t testing.TB) *tests.TestApp { return apiTestFactoryWithGMessages(t) },
ExpectedStatus: http.StatusUnauthorized,
ExpectedContent: []string{"Dashboard authentication is required."},
},
{
Name: "payment API key cannot reauthenticate Google Messages", Method: http.MethodPost,
URL: "/api/connector/gmessages/reauth/google",
Headers: map[string]string{
"Authorization": "Bearer api-secret",
"Content-Type": "application/json",
},
Body: strings.NewReader(`{"cookieData":"SID=missing-rest"}`),
TestAppFactory: func(t testing.TB) *tests.TestApp { return apiTestFactoryWithGMessages(t) },
ExpectedStatus: http.StatusUnauthorized,
ExpectedContent: []string{"Dashboard authentication is required."},
},
}
for i := range scenarios {
scenarios[i].Test(t)
}
}
43 changes: 43 additions & 0 deletions internal/gmessages/client_events.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package gmessages

import (
"sync"

"go.mau.fi/mautrix-gmessages/pkg/libgm"
)

type clientEventGate struct {
mu sync.RWMutex
active bool
}

var clientEventGates sync.Map // map[*libgm.Client]*clientEventGate

func (m *Manager) registerClientEventHandler(client *libgm.Client) {
gate := &clientEventGate{active: true}
clientEventGates.Store(client, gate)
client.SetEventHandler(func(raw any) {
gate.mu.RLock()
defer gate.mu.RUnlock()
if !gate.active {
return
}
m.handleClientEvent(client, raw)
})
}

// retireClient waits for any event handler that already started for this
// client, disables future events, then closes the libgm transport. It must not
// be called from inside that same client's event handler.
func (m *Manager) retireClient(client *libgm.Client) {
if client == nil {
return
}
if value, ok := clientEventGates.LoadAndDelete(client); ok {
gate := value.(*clientEventGate)
gate.mu.Lock()
gate.active = false
gate.mu.Unlock()
}
client.Disconnect()
}
Loading