Add Google Calendar OAuth connect and disconnect
CI / ci (push) Successful in 3m34s
CI / ci (pull_request) Successful in 3m27s

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>
This commit is contained in:
2026-10-07 20:17:20 -04:00
co-authored by Claude Opus 5.5
parent 5ac5982c1f
commit 59d0f57a47
31 changed files with 1817 additions and 67 deletions
+22
View File
@@ -618,6 +618,28 @@ Runs are recorded in `schedule_runs` and executed through `jobs`.
not come back.
- Connected once with Google OAuth in Settings; one event per planned
dinner.
- `internal/gcal` owns the connection, as a confidential OAuth web client
(`MEALQUEST_GOOGLE_CLIENT_ID`, `MEALQUEST_GOOGLE_CLIENT_SECRET_FILE`, both
required at boot) over plain REST — no Google SDK. All three routes are
under the signed-in `/api/v1`:
- `GET /api/v1/integrations/google/connect` redirects to Google's consent
screen with scopes `calendar.events` and `calendar.calendarlist.readonly`,
`access_type=offline`, `prompt=consent` and PKCE. Its `state` travels in
the signed `__Host-mealquest_google` flow cookie (the same
`auth.FlowCookie` mechanism as the login cookie), bound to the session by
a hash of the session's CSRF token.
- `GET /api/v1/integrations/google/callback` (the redirect URI:
`MEALQUEST_PUBLIC_URL`'s origin plus this path) checks the state and
session, exchanges the code, refuses a response with no refresh token
(`reconnect_required`) or without both scopes, seals the refresh token
with `secretbox` into `integrations_google.refresh_token_ct`, sets
`connected_at` and redirects to `/settings/calendar`. A reconnect keeps
the chosen calendar and clears `last_sync_error`.
- `DELETE /api/v1/integrations/google` (CSRF-checked) revokes the token at
Google, then deletes the row; a token Google reports as `invalid_token`
counts as revoked, and any other revoke failure keeps the connection.
- No response, log line or error ever holds a token or Google's response
text.
- **Deterministic event ids**; each plan day records its
`calendar_event_id`.