Skip to content

hir-94: wire up Gmail account connect flow#8

Merged
pypesdev merged 1 commit intopypesdev:mainfrom
jaredzwick:hir-94/gmail-connect-flow
May 3, 2026
Merged

hir-94: wire up Gmail account connect flow#8
pypesdev merged 1 commit intopypesdev:mainfrom
jaredzwick:hir-94/gmail-connect-flow

Conversation

@jaredzwick
Copy link
Copy Markdown
Collaborator

Summary

  • The Gmail Connect button on /dashboard/email-accounts was non-functional. The modal mounted a second GoogleOAuthProvider instead of a button, and POSTed to /api/email-accounts/connect — an endpoint that didn't exist. Even if it had, One Tap returns an ID token that can't be exchanged for refresh tokens.
  • This PR adds the missing endpoint and switches the modal to the standard redirect-based OAuth flow that the existing /oauth/callback already expects.
  • Carved out src/lib/oauthState.ts so the connect route and callback share one source of truth for state encoding (userId + subAgencyId + timestamp, base64-JSON, 10-minute freshness window).

Changes

  • src/app/api/email-accounts/connect/route.ts (new): POST returns { success, authUrl } for provider=gmail. Outlook and IMAP return 501 explicitly so the UI surfaces a clear error.
  • src/lib/oauthState.ts (new): encodeOAuthState / decodeOAuthState with CSRF + freshness checks; rejects empty, malformed, future-dated, and stale states.
  • src/app/api/email-accounts/oauth/callback/route.ts: now consumes decodeOAuthState. Same wire format, same 10-minute window — no behavior change for existing in-flight states.
  • src/components/EmailAccountManagement/index.tsx: removed the broken nested GoogleOAuthProvider, dropped the unused @react-oauth/google import, replaced with a Connect Gmail button that calls the new endpoint and redirects to data.authUrl (mirrors the existing Outlook flow).

Tests

  • tests/int/oauthState.int.spec.ts — 12 specs: round-trip, null subAgency, opaque format, missing userId, explicit timestamp, empty/malformed/wrong-shape rejection, expired, future-dated, missing field coercion, custom max age.
  • tests/int/emailAccountConnect.int.spec.ts — 10 specs: gmail returns authUrl, state contains userId, subAgencyId is embedded, outlook/imap return 501, unknown/missing provider returns 400, unauthenticated returns 401, OAuth helper failure returns 500, OAuth helper not invoked before auth succeeds.
  • All 22 new tests pass. Full pnpm test:int passes everything except the pre-existing api.int.spec.ts failure (missing PAYLOAD_SECRET env on main, unrelated).

Test plan

  • Set NEXT_PUBLIC_GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GMAIL_OAUTH_REDIRECT_URI, GMAIL_ENCRYPTION_KEY, PAYLOAD_SECRET, BETTER_AUTH_SECRET.
  • pnpm dev, sign in, navigate to /dashboard/email-accounts, click Connect Email Account → Gmail → Connect Gmail.
  • Confirm browser redirects to accounts.google.com/o/oauth2/v2/auth?...&state=....
  • Grant consent. Confirm redirect back to /dashboard/email-accounts?success=account_connected&email=....
  • Confirm email_accounts row exists in DB with encrypted access + refresh tokens.
  • Click Connect → Outlook / IMAP → confirm modal still loads (those branches return 501 from the new endpoint, surfaced as "not yet supported").

Out of scope

  • Email sequence builder / send pipeline. The Gmail token + refresh infra (gmailService.ts, tokenRefreshJob.ts, emailQueueProcessor.ts) already exists; HIR-94's first increment is making accounts connectable so those code paths have credentials to use. Sequence-side fixes follow in subsequent PRs.

🤖 Generated with Claude Code

The Gmail "Connect" button was non-functional: the modal mounted
GoogleOAuthProvider where a button should have been, and the frontend
POSTed to a /api/email-accounts/connect endpoint that didn't exist.
Even if it had, Google One Tap returns an ID token that can't be
exchanged for the refresh tokens needed for Gmail send.

This change wires up the redirect-based OAuth flow end-to-end:

- New POST /api/email-accounts/connect returns a Google authorization
  URL with offline access + gmail.send/readonly scopes, signed with a
  state parameter bound to the authenticated user.
- New src/lib/oauthState.ts centralizes state encode/decode (CSRF +
  freshness), and the existing oauth/callback route consumes it.
- The EmailAccountManagement modal now redirects to the auth URL
  instead of mounting an extra OAuthProvider.
- Outlook and IMAP return 501 from /connect with a clear error so the
  UI fails loud instead of silent.

Tests: 22 new vitest specs covering state round-trip, expiry, malformed
input, and every branch of the connect route (gmail/outlook/imap/empty/
unauthorized/oauth-error). Auth is verified before any OAuth work.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants