A disconnect read the stored refresh token, revoked it at Google, then
deleted the row only if it still held that token. Google revokes the whole
grant, so a reconnect whose token was issued before that revoke was kept
as connected with a dead token. Connect's exchange-and-store and
disconnect's read-revoke-delete now hold one mutex across the Google
request and the database write, and the delete is unconditional (the
conditional delete and ErrChanged are gone: the lock is the one guard).
The connect cookie was single-use only in that the browser was asked to
drop it; a saved copy verified again until it expired. The service now
keeps each started connect's state in memory until the cookie expires and
the callback consumes it, so a replayed cookie is refused as bad state.
The fake Google now issues a distinct refresh token per exchange, revokes
the whole grant like Google, and can hold a token or revoke request so
tests can interleave a reconnect and a disconnect.
Maestro task 1e9d7850-3a09-4572-b8a1-cb18cd4774cb (MQ125-01, MQ125-02).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GET /api/v1/integrations/google/connect sends the browser to Google's
consent screen (calendar.events and calendar.calendarlist.readonly,
access_type=offline, prompt=consent, PKCE S256) with a state carried in a
signed __Host-mealquest_google flow cookie bound to the session.
GET /api/v1/integrations/google/callback checks the state and session,
exchanges the code, refuses a response without a refresh token
(reconnect_required) or without both scopes, seals the refresh token with
secretbox into integrations_google and sets connected_at.
DELETE /api/v1/integrations/google revokes the token at Google and clears
it; a revoke failure keeps the connection so it can be retried.
- internal/gcal: the connect service; Google endpoints are overridable and
internal/gcal/gcaltest fakes the token and revoke endpoints.
- internal/auth: the login cookie becomes the exported FlowCookie both
flows use (same wire format); CheckRedirect and ExchangeError are
exported for reuse.
- internal/config: MEALQUEST_GOOGLE_CLIENT_ID and
MEALQUEST_GOOGLE_CLIENT_SECRET_FILE, required at boot.
- DeleteIntegrationsGoogle query (sqlc regenerated); no migration, the
integrations_google table already exists.
- deploy: compose mounts google_client_secret; .env.example and RUNBOOK
document the Google OAuth client, its redirect URI and rotation.
Maestro task 1e9d7850-3a09-4572-b8a1-cb18cd4774cb.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>