Google OAuth connect and disconnect (Feature 16) #125

Member

Task

Maestro task 1e9d7850-3a09-4572-b8a1-cb18cd4774cb — "Google OAuth connect and disconnect" (Feature 16, Google Calendar Sync, task 1 of 5).

Done-condition

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 a state bound to the session; GET /api/v1/integrations/google/callback validates state, exchanges the code (client id from MEALQUEST_GOOGLE_CLIENT_ID, secret from MEALQUEST_GOOGLE_CLIENT_SECRET_FILE, redirect under MEALQUEST_PUBLIC_URL), stores the refresh token with secretbox and sets connected_at; DELETE /api/v1/integrations/google revokes the token at Google and clears it. Tests against an httptest fake Google token/revoke endpoint cover connect, bad state, missing refresh token (error asking to reconnect) and disconnect; no response ever contains the token.

Scope

Connect and disconnect only. The remaining four tasks (calendar list and selection, push to Calendar, token-failure handling, settings screen) are untouched, and the shopping-list trigger belongs to Feature 17 and was deliberately not stubbed.

Points for review

  • Configuration is required at boot (MEALQUEST_GOOGLE_CLIENT_ID, MEALQUEST_GOOGLE_CLIENT_SECRET_FILE), matching the OIDC variables and the fail-fast house rule rather than adding a second "Calendar not configured" path. Consequence, stated by the developer: production cannot start on a release containing this change until the owner creates the Google OAuth web client and sets the secret. The runbook section "Google Calendar OAuth client", deploy/compose.yaml's secret mount and deploy/.env.example were updated in the same commit. Please judge whether boot-required is the right call and whether that deployment coupling is acceptable and adequately documented.
  • The endpoints reuse Feature 03's session-bound state patterns rather than a second mechanism; the callback is reachable in a browser redirect context and the auth requirement chosen for each of the three endpoints is stated in the developer's NOTES.
  • internal/auth/cookie.go, internal/auth/auth.go and the NewRouter signature were touched; all NewRouter call sites in tests were updated.
  • migrations/queries/settings.sql was extended and sqlc regenerated (checked in, no drift); the existing integrations_google table was used, so no new migration.

Evidence (orchestrator-run gate at 59d0f57a47)

  • gofmt -l . — empty; go vet ./... — clean
  • go test -race -count=1 ./... — all packages ok (server 102.5s)
  • CGO_ENABLED=0 go build ./... — ok
  • make sqlc-check SQLC=$(go env GOPATH)/bin/sqlc — no drift
  • cd web && npm run typecheck && npm test && npm run build — PASS, 45 files / 764 tests (no web change)

The developer also reports mutation checks (removing the session-binding, refresh-token or revoke-status check each fails its own test; bypassing the exchange-error redaction fails the leak checks), and that the real binary exits 1 naming both variables when they are missing, and refuses MEALQUEST_GOOGLE_CLIENT_SECRET set inline.

Implementation commit: 59d0f57a47955601d2c21ae4504568650e8d352d

## Task Maestro task `1e9d7850-3a09-4572-b8a1-cb18cd4774cb` — "Google OAuth connect and disconnect" (Feature 16, Google Calendar Sync, task 1 of 5). ## Done-condition `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 a state bound to the session; `GET /api/v1/integrations/google/callback` validates state, exchanges the code (client id from `MEALQUEST_GOOGLE_CLIENT_ID`, secret from `MEALQUEST_GOOGLE_CLIENT_SECRET_FILE`, redirect under `MEALQUEST_PUBLIC_URL`), stores the refresh token with secretbox and sets `connected_at`; `DELETE /api/v1/integrations/google` revokes the token at Google and clears it. Tests against an httptest fake Google token/revoke endpoint cover connect, bad state, missing refresh token (error asking to reconnect) and disconnect; no response ever contains the token. ## Scope Connect and disconnect only. The remaining four tasks (calendar list and selection, push to Calendar, token-failure handling, settings screen) are untouched, and the shopping-list trigger belongs to Feature 17 and was deliberately not stubbed. ## Points for review - **Configuration is required at boot** (`MEALQUEST_GOOGLE_CLIENT_ID`, `MEALQUEST_GOOGLE_CLIENT_SECRET_FILE`), matching the OIDC variables and the fail-fast house rule rather than adding a second "Calendar not configured" path. Consequence, stated by the developer: **production cannot start on a release containing this change until the owner creates the Google OAuth web client and sets the secret.** The runbook section "Google Calendar OAuth client", `deploy/compose.yaml`'s secret mount and `deploy/.env.example` were updated in the same commit. Please judge whether boot-required is the right call and whether that deployment coupling is acceptable and adequately documented. - The endpoints reuse Feature 03's session-bound `state` patterns rather than a second mechanism; the callback is reachable in a browser redirect context and the auth requirement chosen for each of the three endpoints is stated in the developer's NOTES. - `internal/auth/cookie.go`, `internal/auth/auth.go` and the `NewRouter` signature were touched; all `NewRouter` call sites in tests were updated. - `migrations/queries/settings.sql` was extended and sqlc regenerated (checked in, no drift); the existing `integrations_google` table was used, so no new migration. ## Evidence (orchestrator-run gate at 59d0f57a47955601d2c21ae4504568650e8d352d) - `gofmt -l .` — empty; `go vet ./...` — clean - `go test -race -count=1 ./...` — all packages ok (server 102.5s) - `CGO_ENABLED=0 go build ./...` — ok - `make sqlc-check SQLC=$(go env GOPATH)/bin/sqlc` — no drift - `cd web && npm run typecheck && npm test && npm run build` — PASS, 45 files / 764 tests (no web change) The developer also reports mutation checks (removing the session-binding, refresh-token or revoke-status check each fails its own test; bypassing the exchange-error redaction fails the leak checks), and that the real binary exits 1 naming both variables when they are missing, and refuses `MEALQUEST_GOOGLE_CLIENT_SECRET` set inline. Implementation commit: `59d0f57a47955601d2c21ae4504568650e8d352d`
orchestrator-agent added 1 commit 2026-10-07 20:27:40 -04:00
Add Google Calendar OAuth connect and disconnect
CI / ci (push) Successful in 3m34s
CI / ci (pull_request) Successful in 3m27s
59d0f57a47
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>
review_agent requested changes 2026-10-07 20:30:41 -04:00
Dismissed
review_agent left a comment
Member

Independent Codex gpt-6-sol review: CHANGES_REQUIRED for this exact commit.

Independent Codex gpt-6-sol review: CHANGES_REQUIRED for this exact commit.
overseer added 1 commit 2026-10-07 20:47:02 -04:00
Serialize Google connect and disconnect; consume connect state once
CI / ci (push) Successful in 3m32s
CI / ci (pull_request) Successful in 4m2s
2cd2e5af32
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>
review_agent left a comment
Member

Independent Codex gpt-6-sol review: APPROVED for this exact commit.

Independent Codex gpt-6-sol review: APPROVED for this exact commit.
approver_agent merged commit f06b57c921 into feature/c53328cb-577c-4aea-b4b2-817acfc3dfeb-google-calendar-sync 2026-10-07 21:06:30 -04:00
Sign in to join this conversation.
No Reviewers
No labels
3 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Maniacs_Mansion/MealQuest#125