Skip to content

fix: Fix invalidating gmail tokens#692

Merged
elie222 merged 2 commits intoelie222:mainfrom
edulelis:fix-invalidating-gmail-tokens
Aug 18, 2025
Merged

fix: Fix invalidating gmail tokens#692
elie222 merged 2 commits intoelie222:mainfrom
edulelis:fix-invalidating-gmail-tokens

Conversation

@edulelis
Copy link
Collaborator

@edulelis edulelis commented Aug 18, 2025

Summary by CodeRabbit

  • New Features

    • Automatically refreshes Gmail access during permission checks when tokens are invalid/expired.
    • Retries permission verification after a successful refresh.
    • Aligns admin and user permission checks to support refresh tokens.
  • Bug Fixes

    • Improves reliability of Gmail permission verification by handling expired tokens gracefully.
    • Provides clearer feedback when Gmail access has expired and required scopes are missing.
    • Cleans up stale credentials to prevent repeated failures.
    • Non-Google account flows remain unaffected.

@vercel
Copy link

vercel bot commented Aug 18, 2025

@edulelis is attempting to deploy a commit to the Inbox Zero OSS Program Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 18, 2025

Walkthrough

Introduces refresh-token-aware Gmail permission checks. Actions now retrieve both accessToken and tokens.refreshToken from getGmailAndAccessTokenForEmail and pass refreshToken to handleGmailPermissionsCheck. The Gmail permission handler attempts a token refresh on invalid_token before falling back to cleanup that clears tokens and marks access expired.

Changes

Cohort / File(s) Summary
Actions: pass refresh token
apps/web/utils/actions/permissions.ts
Updated to destructure { accessToken, tokens } from getGmailAndAccessTokenForEmail, pass refreshToken: tokens.refreshToken to handleGmailPermissionsCheck in both user and admin flows; non-Google paths unchanged.
Gmail permissions: refresh-and-retry flow
apps/web/utils/gmail/permissions.ts
handleGmailPermissionsCheck signature extended to accept refreshToken. On invalid_token, optionally refreshes via getGmailClientWithRefresh and retries permission check; otherwise performs cleanup by clearing access_token, refresh_token, and expires_at, and returns an error with missingScopes.

Sequence Diagram(s)

sequenceDiagram
  participant Caller as Action (check/admin)
  participant Account as getGmailAndAccessTokenForEmail
  participant Perms as handleGmailPermissionsCheck
  participant Google as Gmail API
  participant DB as Database

  Caller->>Account: fetch { accessToken, tokens.refreshToken }
  Account-->>Caller: credentials
  Caller->>Perms: accessToken, refreshToken, emailAccountId
  Perms->>Google: check permissions with accessToken
  alt permissions ok
    Google-->>Perms: ok
    Perms-->>Caller: result
  else invalid_token
    alt refreshToken provided
      Perms->>Google: refresh via client (force refresh)
      Google-->>Perms: new accessToken
      Perms->>Google: re-check permissions with new token
      Google-->>Perms: result
      Perms-->>Caller: result
    else no refresh or refresh failed
      Perms->>DB: clear access_token, refresh_token, expires_at
      DB-->>Perms: updated
      Perms-->>Caller: error (access expired, missingScopes)
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

A rabbit taps code in the dew-lit dawn,
Refreshing tokens when the old are gone.
If Gmail shrugs with an invalid sigh,
We fetch anew and give it a try.
And should it fail, we sweep the lair—
Clear the burrow, fresh spring air. 🐇✨

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

accessToken: null,
refreshToken,
// force refresh even if existing expiry suggests it's valid
expiresAt: null,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be set to force a token refresh. Otherwise, it will return the client with the invalid access token.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🔭 Outside diff range comments (2)
apps/web/utils/actions/permissions.ts (2)

42-48: Preserve SafeError messages instead of always masking with a generic error.

Throwing a SafeError earlier (e.g., “Gmail access expired. Please reconnect your account.”) is caught here and replaced with “Failed to check permissions”, losing actionable context for the UI.

-    } catch (error) {
-      logger.error("Failed to check permissions", {
-        emailAccountId,
-        error,
-      });
-      throw new SafeError("Failed to check permissions");
-    }
+    } catch (error) {
+      logger.error("Failed to check permissions", { emailAccountId, error });
+      if (error instanceof SafeError) throw error;
+      throw new SafeError("Failed to check permissions");
+    }

75-79: Preserve SafeError messages in the admin action as well.

Same concern as the user action: don’t mask SafeError messages that the UI can handle meaningfully.

-    } catch (error) {
-      logger.error("Admin failed to check permissions", { email, error });
-      throw new SafeError("Failed to check permissions");
-    }
+    } catch (error) {
+      logger.error("Admin failed to check permissions", { email, error });
+      if (error instanceof SafeError) throw error;
+      throw new SafeError("Failed to check permissions");
+    }
🧹 Nitpick comments (4)
apps/web/utils/actions/permissions.ts (1)

36-41: Unify the returned shape to always include hasRefreshToken.

Right now, the branch where hasAllPermissions is false returns only { hasAllPermissions: false }, while other branches include hasRefreshToken. This inconsistency complicates consumers.

Consider simplifying to compute hasRefreshToken once and always return both fields.

-      if (!hasAllPermissions) return { hasAllPermissions: false };
-
-      if (!tokens.refreshToken)
-        return { hasRefreshToken: false, hasAllPermissions };
-
-      return { hasRefreshToken: true, hasAllPermissions };
+      const hasRefreshToken = Boolean(tokens.refreshToken);
+      return { hasAllPermissions, hasRefreshToken };
apps/web/utils/gmail/permissions.ts (3)

33-35: Avoid caching the token introspection request.

Next.js’ fetch can cache under some circumstances; token status must be real-time. Add cache: "no-store".

-    const response = await fetch(
-      `https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=${accessToken}`,
-    );
+    const response = await fetch(
+      `https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=${accessToken}`,
+      { cache: "no-store" },
+    );

105-108: Log refresh failures for better observability.

Currently the refresh failure is silently swallowed here; adding a warn with context helps triage.

-      } catch (_) {
-        // getGmailClientWithRefresh, getAccessTokenFromClient will throw if access token is invalid
-        // Refresh failed, fall through to cleanup
-      }
+      } catch (err) {
+        // getGmailClientWithRefresh, getAccessTokenFromClient will throw if access token is invalid
+        logger.warn("Failed to refresh Gmail token during permissions check", {
+          emailAccountId,
+          error: err,
+        });
+        // Refresh failed, fall through to cleanup
+      }

116-118: Return shape consistency: include missingScopes on “Email account not found”.

All other paths include missingScopes; keep the surface area consistent.

-    if (!emailAccount)
-      return { hasAllPermissions: false, error: "Email account not found" };
+    if (!emailAccount)
+      return {
+        hasAllPermissions: false,
+        error: "Email account not found",
+        missingScopes,
+      };
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 7bbc9cd and 15049ed.

📒 Files selected for processing (2)
  • apps/web/utils/actions/permissions.ts (2 hunks)
  • apps/web/utils/gmail/permissions.ts (2 hunks)
🧰 Additional context used
📓 Path-based instructions (11)
apps/web/**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (apps/web/CLAUDE.md)

apps/web/**/*.{ts,tsx}: Use TypeScript with strict null checks
Path aliases: Use @/ for imports from project root
Use proper error handling with try/catch blocks
Format code with Prettier
Leverage TypeScript inference for better DX

Files:

  • apps/web/utils/gmail/permissions.ts
  • apps/web/utils/actions/permissions.ts
!{.cursor/rules/*.mdc}

📄 CodeRabbit Inference Engine (.cursor/rules/cursor-rules.mdc)

Never place rule files in the project root, in subdirectories outside .cursor/rules, or in any other location

Files:

  • apps/web/utils/gmail/permissions.ts
  • apps/web/utils/actions/permissions.ts
**/*.ts

📄 CodeRabbit Inference Engine (.cursor/rules/form-handling.mdc)

**/*.ts: The same validation should be done in the server action too
Define validation schemas using Zod

Files:

  • apps/web/utils/gmail/permissions.ts
  • apps/web/utils/actions/permissions.ts
apps/web/utils/gmail/**/*.ts

📄 CodeRabbit Inference Engine (.cursor/rules/gmail-api.mdc)

Keep provider-specific implementation details isolated in the appropriate utils subfolder (e.g., 'apps/web/utils/gmail/')

Files:

  • apps/web/utils/gmail/permissions.ts
**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (.cursor/rules/logging.mdc)

**/*.{ts,tsx}: Use createScopedLogger for logging in backend TypeScript files
Typically add the logger initialization at the top of the file when using createScopedLogger
Only use .with() on a logger instance within a specific function, not for a global logger

Import Prisma in the project using import prisma from "@/utils/prisma";

**/*.{ts,tsx}: Don't use TypeScript enums.
Don't use TypeScript const enum.
Don't use the TypeScript directive @ts-ignore.
Don't use primitive type aliases or misleading types.
Don't use empty type parameters in type aliases and interfaces.
Don't use any or unknown as type constraints.
Don't use implicit any type on variable declarations.
Don't let variables evolve into any type through reassignments.
Don't use non-null assertions with the ! postfix operator.
Don't misuse the non-null assertion operator (!) in TypeScript files.
Don't use user-defined types.
Use as const instead of literal types and type annotations.
Use export type for types.
Use import type for types.
Don't declare empty interfaces.
Don't merge interfaces and classes unsafely.
Don't use overload signatures that aren't next to each other.
Use the namespace keyword instead of the module keyword to declare TypeScript namespaces.
Don't use TypeScript namespaces.
Don't export imported variables.
Don't add type annotations to variables, parameters, and class properties that are initialized with literal expressions.
Don't use parameter properties in class constructors.
Use either T[] or Array consistently.
Initialize each enum member value explicitly.
Make sure all enum members are literal values.

Files:

  • apps/web/utils/gmail/permissions.ts
  • apps/web/utils/actions/permissions.ts
apps/web/utils/**

📄 CodeRabbit Inference Engine (.cursor/rules/project-structure.mdc)

Create utility functions in utils/ folder for reusable logic

Files:

  • apps/web/utils/gmail/permissions.ts
  • apps/web/utils/actions/permissions.ts
apps/web/utils/**/*.ts

📄 CodeRabbit Inference Engine (.cursor/rules/project-structure.mdc)

apps/web/utils/**/*.ts: Use lodash utilities for common operations (arrays, objects, strings)
Import specific lodash functions to minimize bundle size

Files:

  • apps/web/utils/gmail/permissions.ts
  • apps/web/utils/actions/permissions.ts
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit Inference Engine (.cursor/rules/ultracite.mdc)

**/*.{js,jsx,ts,tsx}: Don't use elements in Next.js projects.
Don't use elements in Next.js projects.
Don't use namespace imports.
Don't access namespace imports dynamically.
Don't use global eval().
Don't use console.
Don't use debugger.
Don't use var.
Don't use with statements in non-strict contexts.
Don't use the arguments object.
Don't use consecutive spaces in regular expression literals.
Don't use the comma operator.
Don't use unnecessary boolean casts.
Don't use unnecessary callbacks with flatMap.
Use for...of statements instead of Array.forEach.
Don't create classes that only have static members (like a static namespace).
Don't use this and super in static contexts.
Don't use unnecessary catch clauses.
Don't use unnecessary constructors.
Don't use unnecessary continue statements.
Don't export empty modules that don't change anything.
Don't use unnecessary escape sequences in regular expression literals.
Don't use unnecessary labels.
Don't use unnecessary nested block statements.
Don't rename imports, exports, and destructured assignments to the same name.
Don't use unnecessary string or template literal concatenation.
Don't use String.raw in template literals when there are no escape sequences.
Don't use useless case statements in switch statements.
Don't use ternary operators when simpler alternatives exist.
Don't use useless this aliasing.
Don't initialize variables to undefined.
Don't use the void operators (they're not familiar).
Use arrow functions instead of function expressions.
Use Date.now() to get milliseconds since the Unix Epoch.
Use .flatMap() instead of map().flat() when possible.
Use literal property access instead of computed property access.
Don't use parseInt() or Number.parseInt() when binary, octal, or hexadecimal literals work.
Use concise optional chaining instead of chained logical expressions.
Use regular expression literals instead of the RegExp constructor when possible.
Don't use number literal object member names th...

Files:

  • apps/web/utils/gmail/permissions.ts
  • apps/web/utils/actions/permissions.ts
!pages/_document.{js,jsx,ts,tsx}

📄 CodeRabbit Inference Engine (.cursor/rules/ultracite.mdc)

!pages/_document.{js,jsx,ts,tsx}: Don't import next/document outside of pages/_document.jsx in Next.js projects.
Don't import next/document outside of pages/_document.jsx in Next.js projects.

Files:

  • apps/web/utils/gmail/permissions.ts
  • apps/web/utils/actions/permissions.ts
apps/web/utils/actions/**/*.ts

📄 CodeRabbit Inference Engine (apps/web/CLAUDE.md)

apps/web/utils/actions/**/*.ts: Use server actions for all mutations (create/update/delete operations)
next-safe-action provides centralized error handling
Use Zod schemas for validation on both client and server
Use revalidatePath in server actions for cache invalidation

apps/web/utils/actions/**/*.ts: Use server actions (with next-safe-action) for all mutations (create/update/delete operations); do NOT use POST API routes for mutations.
Use revalidatePath in server actions to invalidate cache after mutations.

Files:

  • apps/web/utils/actions/permissions.ts
apps/web/utils/actions/*.ts

📄 CodeRabbit Inference Engine (.cursor/rules/server-actions.mdc)

apps/web/utils/actions/*.ts: Implement all server actions using the next-safe-action library for type safety, input validation, context management, and error handling. Refer to apps/web/utils/actions/safe-action.ts for client definitions (actionClient, actionClientUser, adminActionClient).
Use actionClientUser when only authenticated user context (userId) is needed.
Use actionClient when both authenticated user context and a specific emailAccountId are needed. The emailAccountId must be bound when calling the action from the client.
Use adminActionClient for actions restricted to admin users.
Access necessary context (like userId, emailAccountId, etc.) provided by the safe action client via the ctx object in the .action() handler.
Server Actions are strictly for mutations (operations that change data, e.g., creating, updating, deleting). Do NOT use Server Actions for data fetching (GET operations). For data fetching, use dedicated GET API Routes combined with SWR Hooks.
Use SafeError for expected/handled errors within actions if needed. next-safe-action provides centralized error handling.
Use the .metadata({ name: "actionName" }) method to provide a meaningful name for monitoring. Sentry instrumentation is automatically applied via withServerActionInstrumentation within the safe action clients.
If an action modifies data displayed elsewhere, use revalidatePath or revalidateTag from next/cache within the action handler as needed.

Server action files must start with use server

Files:

  • apps/web/utils/actions/permissions.ts
🧬 Code Graph Analysis (2)
apps/web/utils/gmail/permissions.ts (1)
apps/web/utils/gmail/client.ts (2)
  • getGmailClientWithRefresh (49-103)
  • getAccessTokenFromClient (117-122)
apps/web/utils/actions/permissions.ts (2)
apps/web/utils/account.ts (1)
  • getGmailAndAccessTokenForEmail (29-43)
apps/web/utils/gmail/permissions.ts (1)
  • handleGmailPermissionsCheck (75-135)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Static Code Analysis Js
  • GitHub Check: Secret Detection
  • GitHub Check: Jit Security
🔇 Additional comments (6)
apps/web/utils/actions/permissions.ts (3)

28-32: Passing refreshToken into the permissions check is correct and enables refresh-on-invalid flow.

Good wiring: forwarding tokens.refreshToken to handleGmailPermissionsCheck unlocks the new refresh path and aligns with the updated client utilities.


63-66: Admin path now uses getGmailAndAccessTokenForEmail — good alignment with the new token shape.

This ensures admin checks also benefit from the updated access token derivation and token bundle.


68-72: Good: admin check passes refreshToken into handleGmailPermissionsCheck.

This ensures the refresh-on-invalid path is available in admin-triggered checks as well.

apps/web/utils/gmail/permissions.ts (3)

2-5: Imports for refresh-capable Gmail client are correctly introduced.

Bringing in getAccessTokenFromClient and getGmailClientWithRefresh is necessary for the refresh-and-retry flow.


75-82: Signature now accepts refreshToken — aligns with callers.

Type is properly nullable, matching upstream availability.


88-110: Refresh-and-retry path on invalid_token looks solid.

  • Forces a refresh even if expiry suggests validity.
  • Re-runs the permissions check with the fresh access token.
  • Falls back to cleanup if refresh isn’t possible.

Nice failover flow.

@elie222 elie222 merged commit aa66637 into elie222:main Aug 18, 2025
8 of 10 checks passed
@edulelis edulelis deleted the fix-invalidating-gmail-tokens branch August 27, 2025 20:32
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

Comments