hir-94: wire up Gmail account connect flow#8
Merged
pypesdev merged 1 commit intopypesdev:mainfrom May 3, 2026
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
/dashboard/email-accountswas non-functional. The modal mounted a secondGoogleOAuthProviderinstead 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./oauth/callbackalready expects.src/lib/oauthState.tsso 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 }forprovider=gmail. Outlook and IMAP return 501 explicitly so the UI surfaces a clear error.src/lib/oauthState.ts(new):encodeOAuthState/decodeOAuthStatewith CSRF + freshness checks; rejects empty, malformed, future-dated, and stale states.src/app/api/email-accounts/oauth/callback/route.ts: now consumesdecodeOAuthState. Same wire format, same 10-minute window — no behavior change for existing in-flight states.src/components/EmailAccountManagement/index.tsx: removed the broken nestedGoogleOAuthProvider, dropped the unused@react-oauth/googleimport, replaced with aConnect Gmailbutton that calls the new endpoint and redirects todata.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.pnpm test:intpasses everything except the pre-existingapi.int.spec.tsfailure (missingPAYLOAD_SECRETenv on main, unrelated).Test plan
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.accounts.google.com/o/oauth2/v2/auth?...&state=..../dashboard/email-accounts?success=account_connected&email=....email_accountsrow exists in DB with encrypted access + refresh tokens.Out of scope
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