Skip to content

Add Google Calendar integration with OAuth and sync - #369

Merged
2witstudios merged 8 commits into
masterfrom
claude/google-calendar-sync-research-d0IiX
Feb 4, 2026
Merged

2witstudios merged 8 commits into
masterfrom
claude/google-calendar-sync-research-d0IiX

Conversation

@2witstudios

@2witstudios 2witstudios commented Feb 4, 2026 •

Copy link
Copy Markdown
Owner

Summary

Implements complete Google Calendar integration allowing users to import calendar events into PageSpace. Includes OAuth 2.0 authentication flow, event synchronization, and connection management.

Key Changes

API Routes

  • /api/integrations/google-calendar/connect - Initiates OAuth flow with CSRF protection and rate limiting
  • /api/integrations/google-calendar/callback - Handles OAuth callback, exchanges auth code for tokens, stores encrypted credentials
  • /api/integrations/google-calendar/status - Returns connection status and sync metadata
  • /api/integrations/google-calendar/sync - Triggers manual calendar sync
  • /api/integrations/google-calendar/disconnect - Revokes tokens and disconnects integration

Frontend

  • /settings/integrations/google-calendar - Settings page for managing Google Calendar connection with sync status, last sync time, and error handling
  • Updated /settings to include Google Calendar integration link

Libraries

  • google-calendar/api-client.ts - Pure functions for Google Calendar API interactions (list calendars, list events, get event)
  • google-calendar/sync-service.ts - Handles incremental sync logic with cursor tracking and conflict resolution

Implementation Details

Security

  • Zero-trust encryption: All tokens encrypted before storage using @pagespace/lib encryption
  • CSRF protection: State parameter signed with HMAC-SHA256, validated on callback
  • State validation: 10-minute expiration window, signature verification prevents tampering
  • Token revocation: Best-effort revocation on disconnect via Google OAuth endpoint
  • Rate limiting: Connection attempts limited per user

OAuth Flow

  • Uses offline access type to obtain refresh tokens
  • Requests calendar.readonly scope for read-only access
  • Implements incremental authorization with include_granted_scopes
  • Forces consent prompt to ensure refresh token is returned
  • Includes user email as login hint for better UX

Sync Strategy

  • Supports incremental sync using Google's syncToken for efficiency
  • Falls back to full sync on token expiration (410 status)
  • Tracks lastSyncAt, lastSyncError, and syncCursor for monitoring
  • Configurable sync frequency (default 15 minutes)
  • Marks imported events as read-only by default

Data Storage

  • Upserts connection per user (one connection per user)
  • Stores encrypted access and refresh tokens
  • Tracks token expiration for proactive refresh
  • Maintains sync metadata (cursor, last sync time, errors)
  • Preserves connection record on disconnect for audit trail

Error Handling

  • Comprehensive error messages for OAuth failures (access denied, invalid state, expired state, etc.)
  • User-friendly error display on settings page
  • Logging of all auth events for debugging
  • Graceful degradation when Google API calls fail

https://claude.ai/code/session_01WcRaPPV1DmhjznbYLxLXP9

Summary by CodeRabbit

  • New Features

    • Connect Google Calendar via secure OAuth from Settings; view connection status, connect/disconnect, and trigger syncs.
    • Automatic initial import and manual per-calendar incremental sync with user-visible sync results and error messages.
    • Calendar UI now surfaces "Import from Google Calendar" prompts on desktop and mobile.
  • Chores

    • Database schema extended to store Google Calendar connections, tokens, sync state, and event metadata.
  • Documentation

    • Added integration roadmap and sync design doc.

Comprehensive planning document for optional Google Calendar integration:
- Reuses existing Google OAuth (just adds calendar scope)
- Settings page integration with connect/disconnect flow
- Subtle calendar empty state suggestion for unconnected users
- Phased approach: import-only → export → two-way sync
- Database schema for token storage and sync state tracking

https://claude.ai/code/session_01WcRaPPV1DmhjznbYLxLXP9
- Add googleCalendarConnections table for OAuth tokens and sync config
- Extend calendarEvents with Google sync tracking fields:
  - googleEventId, googleCalendarId for sync matching
  - syncedFromGoogle flag for imported events
  - lastGoogleSync timestamp
  - googleSyncReadOnly to prevent editing synced events
- Generate migration 0066_soft_tana_nile.sql

https://claude.ai/code/session_01WcRaPPV1DmhjznbYLxLXP9
OAuth endpoints:
- /api/integrations/google-calendar/connect - initiate calendar.readonly OAuth
- /api/integrations/google-calendar/callback - handle callback, encrypt & store tokens

Token management (zero-trust):
- Encrypted storage using AES-256-GCM
- Automatic token refresh before expiration
- Connection status tracking (active/expired/error)

API client (pure functions):
- listCalendars, listEvents, getEvent
- Pagination handling
- Error classification (auth errors, rate limits)

Event transformation:
- Google → PageSpace event mapping
- Recurrence rule parsing (RRULE → JSON)
- HTML description sanitization
- Color and visibility mapping

https://claude.ai/code/session_01WcRaPPV1DmhjznbYLxLXP9
Settings page:
- Add Google Calendar entry to settings navigation
- Create settings page with connect/disconnect/sync UI
- Show connection status, last sync time, and errors
- Privacy information card

API endpoints:
- /status - Get connection status for settings page
- /disconnect - Revoke OAuth, clear tokens, update events
- /sync - Trigger manual sync

Sync service:
- Full sync for initial connection
- Incremental sync using Google's sync tokens
- Upsert logic for events (create/update/delete)
- Handle cancelled events as soft deletes

https://claude.ai/code/session_01WcRaPPV1DmhjznbYLxLXP9
Calendar views:
- Add subtle "Import from Google Calendar" link in empty states
- Show in AgendaView and MobileCalendarView

AI calendar tools:
- Update canEditEvent to check for Google sync read-only flag
- Return descriptive error messages for synced events
- Prevent editing/deleting read-only synced events

https://claude.ai/code/session_01WcRaPPV1DmhjznbYLxLXP9
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@coderabbitai

coderabbitai Bot commented Feb 4, 2026 •

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds a complete Google Calendar integration: OAuth connect/callback routes, token refresh and status management, per-user incremental/full sync service, DB schema and migrations for connections/events, API endpoints (connect/status/sync/disconnect), UI settings and calendar import hints, and pure utilities for API and event transformation.

Changes

Cohort / File(s) Summary
OAuth API Routes
apps/web/src/app/api/integrations/google-calendar/connect/route.ts, apps/web/src/app/api/integrations/google-calendar/callback/route.ts
Initiate OAuth (signed HMAC state, rate-limit, login_hint) and handle callback (verify state, exchange code, fetch userinfo, encrypt tokens, upsert connection, redirect with result).
Connection Management Routes
apps/web/src/app/api/integrations/google-calendar/disconnect/route.ts, apps/web/src/app/api/integrations/google-calendar/status/route.ts, apps/web/src/app/api/integrations/google-calendar/sync/route.ts
Disconnect (best-effort revoke/clear creds), return detailed connection status, and trigger on-demand sync with result payloads and error handling.
Settings UI
apps/web/src/app/settings/integrations/google-calendar/page.tsx, apps/web/src/app/settings/page.tsx
New Google Calendar settings page and settings listing: connect/disconnect controls, sync now, auto-sync on connect, status badges, and error handling/UI flows.
Calendar UI Hints
apps/web/src/components/calendar/AgendaView.tsx, apps/web/src/components/calendar/MobileCalendarView.tsx
Add optional "Import from Google Calendar" links/icons in empty states and day groups; add showGoogleCalendarHint prop to AgendaView.
Google API Client
apps/web/src/lib/integrations/google-calendar/api-client.ts
Typed HTTP helpers and IO functions for calendar/event listing and fetching, pagination and sync-token handling, unified result type with reauth hints.
Event Transformation
apps/web/src/lib/integrations/google-calendar/event-transform.ts
Pure transformers: date parsing, visibility/color mapping, description sanitization, RRULE parsing, transform Google events to PageSpace shape, and sync-needed checks.
Sync Service
apps/web/src/lib/integrations/google-calendar/sync-service.ts
Orchestrates per-calendar incremental/full syncs, manages per-calendar sync cursors, transforms and upserts/deletes events, persists sync metadata, and aggregates counts.
Token Management
apps/web/src/lib/integrations/google-calendar/token-refresh.ts
Token expiry detection, refresh flow with Google, encrypted token update, connection status updates, and getValidAccessToken helper returning reauth hints.
Integration Barrel
apps/web/src/lib/integrations/google-calendar/index.ts
Re-exports token helpers, API client, event-transform, and sync-service public APIs and types.
AI Calendar Tools
apps/web/src/lib/ai/tools/calendar-write-tools.ts
canEditEvent signature changed to return { canEdit, reason? }; callers updated to use structured result.
Database Schema & Migration
packages/db/drizzle/0066_soft_tana_nile.sql, packages/db/drizzle/meta/_journal.json, packages/db/src/schema/calendar.ts
Add google_calendar_connections table and enum status, indices, FK constraints; extend calendar_events with google sync fields (googleEventId, googleCalendarId, syncedFromGoogle, lastGoogleSync, googleSyncReadOnly); add migration journal entry.
Docs / Task Epic
tasks/google-calendar-sync.md
New integration epic describing design, sync strategy, schema, UI flows, phased roadmap, and error handling considerations.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Client as Client/Browser
    participant PageSpace as PageSpace API
    participant Google as Google OAuth
    participant DB as Database

    User->>Client: Click "Connect Google Calendar"
    Client->>PageSpace: POST /api/integrations/google-calendar/connect
    PageSpace->>PageSpace: Auth check, rate-limit, build signed state
    PageSpace->>Client: Return OAuth URL
    Client->>Google: Redirect to Google OAuth
    Google->>Client: Redirect callback with code & state
    Client->>PageSpace: GET /api/integrations/google-calendar/callback?code=&state=
    PageSpace->>PageSpace: Verify HMAC state and expiry
    PageSpace->>Google: Exchange code for tokens
    Google->>PageSpace: Return tokens
    PageSpace->>Google: Fetch userinfo
    Google->>PageSpace: Return user details
    PageSpace->>PageSpace: Encrypt tokens, upsert connection
    PageSpace->>DB: Persist google_calendar_connections
    PageSpace->>Client: Redirect to returnUrl with connected flag
Loading
sequenceDiagram
    actor User
    participant Client as Client/Browser
    participant PageSpace as PageSpace API
    participant GoogleAPI as Google Calendar API
    participant DB as Database

    User->>Client: Click "Sync Now"
    Client->>PageSpace: POST /api/integrations/google-calendar/sync
    PageSpace->>DB: Load connection & sync cursors
    PageSpace->>PageSpace: getValidAccessToken(userId)
    alt refresh required
        PageSpace->>GoogleAPI: Token refresh request
        GoogleAPI->>PageSpace: New access token
        PageSpace->>DB: Update encrypted token & expiry
    end
    loop per selected calendar
        PageSpace->>GoogleAPI: listEvents(calendarId, syncToken?)
        alt syncToken expired (410)
            GoogleAPI->>PageSpace: 410 -> full sync fallback
        else
            GoogleAPI->>PageSpace: Return events & nextSyncToken
        end
        PageSpace->>PageSpace: Transform events to PageSpace format
        loop each event
            PageSpace->>DB: Create/Update/Delete calendar_events
        end
        PageSpace->>DB: Persist updated syncCursor & lastSyncAt
    end
    PageSpace->>Client: Return sync result counts
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I hopped to Google, signed my state,
Keys encrypted, tidy and great,
Events sneaked in with gentle cheer,
Syncs that hum and logs that clear,
Hop-hop — your calendar’s now near! 🥕📆

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 72.73% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add Google Calendar integration with OAuth and sync' accurately and clearly describes the main change, which is the implementation of Google Calendar integration with OAuth authentication and event synchronization.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch claude/google-calendar-sync-research-d0IiX

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

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@apps/web/src/app/api/integrations/google-calendar/callback/route.ts`:
- Around line 181-184: The log currently includes raw Google email via
loggers.auth.info('Google Calendar connected successfully', { userId,
googleEmail })—replace raw logging with a non-PII representation: compute a
maskedEmail (e.g., keep first char of local-part and domain with stars for the
rest) or an irreversible hash (e.g., sha256) and log that instead (e.g.,
maskedEmail or emailHash) along with userId; update the logging call in the
Google Calendar callback route (where loggers.auth.info is invoked) and ensure
the original googleEmail is not written to logs or persisted.

In `@apps/web/src/app/api/integrations/google-calendar/connect/route.ts`:
- Around line 46-51: The handler currently calls req.json() directly which will
throw on empty or invalid JSON and lead to a 500; wrap the JSON parsing in a
try/catch (or pre-check Content-Type) before calling connectSchema.safeParse,
detect parse errors or an empty body, and return a 400 Response.json with a
clear error (e.g., "Invalid or empty JSON body") instead of allowing the error
to propagate; update the route's error handling so only unexpected errors still
return 500 while JSON parse/validation failures use 400 (references: req.json(),
connectSchema.safeParse, the route handler logic).

In `@apps/web/src/app/settings/integrations/google-calendar/page.tsx`:
- Around line 60-68: The effect references handleSync but doesn't include it in
the dependency array, causing potential stale closures; update the code by
memoizing handleSync with useCallback (including csrfToken and any other used
state/props in its dependency list) or move the sync logic into a stable
function, then add handleSync to the useEffect dependency array so the effect
uses the current closure; refer to handleSync and the useEffect block around
justConnected/router to locate where to apply useCallback or extraction.

In `@apps/web/src/lib/integrations/google-calendar/sync-service.ts`:
- Around line 78-106: The current loop over calendarsToSync writes each
calendarResult.syncCursor into the single connection.syncCursor field, causing
later cursors to overwrite earlier ones; update the logic to store per-calendar
cursors (e.g., a connection.syncCursors map keyed by calendarId) so each
calendar's token is preserved: read connection.syncCursors (or initialize {}),
use the existing per-calendar token when calling syncCalendar, and after a
successful sync set cursors[calendarId] = calendarResult.syncCursor and persist
that JSON map via the update to googleCalendarConnections (also update
lastSyncAt/lastSyncError/updatedAt as before); ensure code paths that read a
sync token pass cursors[calendarId] into syncCalendar instead of
connection.syncCursor.
🧹 Nitpick comments (15)
apps/web/src/app/api/integrations/google-calendar/callback/route.ts (1)

60-67: Use constant-time compare for the OAuth state HMAC.

This avoids timing side‑channels when validating the signature. Please verify the Node runtime supports crypto.timingSafeEqual.

🔐 Suggested hardening
       const expectedSignature = crypto
         .createHmac('sha256', process.env.OAUTH_STATE_SECRET!)
         .update(JSON.stringify(stateWithSignature.data))
         .digest('hex');

-      if (stateWithSignature.sig !== expectedSignature) {
+      const providedSignature = stateWithSignature.sig;
+      const expectedBuffer = Buffer.from(expectedSignature, 'hex');
+      const providedBuffer = Buffer.from(providedSignature, 'hex');
+
+      if (
+        expectedBuffer.length !== providedBuffer.length ||
+        !crypto.timingSafeEqual(expectedBuffer, providedBuffer)
+      ) {
         loggers.auth.warn('Google Calendar OAuth state signature mismatch');
         return NextResponse.redirect(
           new URL('/settings/integrations/google-calendar?error=invalid_state', baseUrl)
         );
       }
apps/web/src/app/api/integrations/google-calendar/disconnect/route.ts (2)

32-46: Token revocation response is not checked.

The token revocation request is made but its response status is not validated. While this is best-effort, checking the response could help distinguish between "already revoked" (which is fine) and actual network/server errors for better logging.

🔧 Proposed improvement for more informative logging
     try {
       const accessToken = await decrypt(connection.accessToken);
-      await fetch(`https://oauth2.googleapis.com/revoke?token=${accessToken}`, {
+      const revokeResponse = await fetch(`https://oauth2.googleapis.com/revoke?token=${accessToken}`, {
         method: 'POST',
         headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
       });
-      loggers.auth.info('Google Calendar token revoked', { userId });
+      if (revokeResponse.ok) {
+        loggers.auth.info('Google Calendar token revoked', { userId });
+      } else {
+        loggers.auth.warn('Google token revocation returned non-OK status', {
+          userId,
+          status: revokeResponse.status,
+        });
+      }
     } catch (error) {

48-60: Consider also clearing tokenExpiresAt.

When clearing sensitive token data, the tokenExpiresAt field retains timing information about the revoked tokens. For completeness, consider setting it to a past date or null to indicate the tokens are no longer valid.

🔧 Proposed fix
       .set({
         status: 'disconnected',
         statusMessage: 'Disconnected by user',
         // Clear tokens for security (they're revoked anyway)
         accessToken: 'REVOKED',
         refreshToken: 'REVOKED',
+        tokenExpiresAt: new Date(0), // Mark as expired
         updatedAt: new Date(),
       })
apps/web/src/app/api/integrations/google-calendar/connect/route.ts (1)

55-67: Unused clientIP variable in rate limiting.

The clientIP is fetched but the rate limit is keyed only by userId. If the intent is user-level rate limiting (which is reasonable), the clientIP fetch on line 56 is only needed for logging. This is fine, but worth noting for clarity.

tasks/google-calendar-sync.md (1)

24-30: Add language specifiers to fenced code blocks.

The ASCII wireframes/diagrams at lines 24, 34, and 64 lack language specifiers. While these are visual diagrams rather than code, adding a language hint like text or plaintext satisfies markdown linting rules and improves accessibility for screen readers.

📝 Example fix for line 24
-```
+```text
 Google Calendar Sync
 ├── Icon: Calendar + Google colors

Also applies to: 34-58, 64-74

apps/web/src/app/settings/integrations/google-calendar/page.tsx (1)

163-193: Consider adding a confirmation dialog for disconnect.

Per the epic requirements (tasks/google-calendar-sync.md line 277-278), users should see a confirmation dialog explaining data retention before disconnecting. This helps prevent accidental disconnects.

apps/web/src/lib/integrations/google-calendar/sync-service.ts (1)

179-196: Recursive fallback for 410 could theoretically cause deep recursion.

If the Google API repeatedly returns 410 (e.g., due to a bug or misconfiguration), the recursive call to syncCalendar with undefined syncToken could fail again and throw, but a truly persistent 410 on non-token requests would indicate a deeper issue. Consider adding a guard to prevent unintended recursion.

🛡️ Proposed guard against repeated 410s
 const syncCalendar = async (
   userId: string,
   accessToken: string,
   calendarId: string,
   targetDriveId: string | null,
   markAsReadOnly: boolean,
   syncToken: string | undefined | null,
   timeMin: Date,
-  timeMax: Date
+  timeMax: Date,
+  isRetry = false
 ): Promise<...> => {
   // ...
   if (!listResult.success) {
     // If sync token is invalid, fall back to full sync
-    if (listResult.statusCode === 410) {
+    if (listResult.statusCode === 410 && !isRetry) {
       loggers.api.info('Sync token expired, performing full sync', { userId, calendarId });
       return syncCalendar(
         // ... params
-        undefined, // No sync token
+        undefined,
         timeMin,
-        timeMax
+        timeMax,
+        true // Mark as retry
       );
     }
packages/db/src/schema/calendar.ts (1)

138-180: Well-structured OAuth token storage with appropriate security considerations.

The schema correctly:

  • Enforces one connection per user via unique constraint on userId
  • Uses cascade delete for user cleanup
  • Uses set null for drive references (preserving connections if drive is deleted)
  • Stores encrypted tokens (as documented in PR objectives)

Consider: The userIdx index on line 176 is redundant since userId already has a unique constraint (line 145), which implicitly creates an index. You could remove this index to reduce storage overhead.

♻️ Optional: Remove redundant index
 }, (table) => {
   return {
-    userIdx: index('google_calendar_connections_user_id_idx').on(table.userId),
     statusIdx: index('google_calendar_connections_status_idx').on(table.status),
     targetDriveIdx: index('google_calendar_connections_target_drive_id_idx').on(table.targetDriveId),
   }
 });
apps/web/src/lib/integrations/google-calendar/event-transform.ts (2)

38-58: Consider logging when fallback is used in parseGoogleDateTime.

The fallback to current time (lines 56-57) silently handles invalid input. While defensive, this could mask data issues from Google's API. Consider whether silent fallback is appropriate or if this should be logged/flagged.


148-154: Unsafe type assertion on BYDAY parsing.

The cast on line 150 assumes all split values are valid day codes, but invalid values could slip through. The filter on lines 151-153 helps, but the type assertion happens before filtering.

♻️ Safer BYDAY parsing
   // Parse BYDAY (for weekly)
   if (ruleMap.BYDAY) {
-    const days = ruleMap.BYDAY.split(',') as ('MO' | 'TU' | 'WE' | 'TH' | 'FR' | 'SA' | 'SU')[];
-    rule.byDay = days.filter((d) =>
-      ['MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU'].includes(d)
-    );
+    const validDays = ['MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU'] as const;
+    rule.byDay = ruleMap.BYDAY.split(',')
+      .filter((d): d is typeof validDays[number] => validDays.includes(d as typeof validDays[number]));
   }
apps/web/src/lib/integrations/google-calendar/token-refresh.ts (3)

48-85: Token refresh implementation looks correct.

Uses the official google-auth-library OAuth2Client properly. The 1-hour default expiry fallback (line 75) is reasonable when Google doesn't return expiry_date.

Note: The error as Error cast (line 82) may lose information if the error isn't an Error instance. Consider using a safer pattern.

♻️ Safer error handling
   } catch (error) {
-    loggers.auth.error('Token refresh failed', error as Error);
+    loggers.auth.error('Token refresh failed', error instanceof Error ? error : new Error(String(error)));
     return null;
   }

147-164: Redundant updatedAt assignment.

The schema defines updatedAt with $onUpdate(() => new Date()) which auto-updates on modifications. Setting it manually in line 158 is redundant.

♻️ Remove redundant updatedAt
     await db
       .update(googleCalendarConnections)
       .set({
         accessToken: encryptedAccessToken,
         tokenExpiresAt: refreshResult.expiresAt,
         status: 'active',
         statusMessage: null,
-        updatedAt: new Date(),
       })
       .where(eq(googleCalendarConnections.userId, userId));

174-187: Same redundant updatedAt in updateConnectionStatus.

Same issue as above - the schema handles updatedAt automatically.

♻️ Remove redundant updatedAt
 export const updateConnectionStatus = async (
   userId: string,
   status: GoogleCalendarConnection['status'],
   statusMessage: string | null
 ): Promise<void> => {
   await db
     .update(googleCalendarConnections)
     .set({
       status,
       statusMessage,
-      updatedAt: new Date(),
     })
     .where(eq(googleCalendarConnections.userId, userId));
 };
apps/web/src/lib/integrations/google-calendar/api-client.ts (2)

166-221: Consider adding a timeout to prevent hanging requests.

The fetch call has no timeout, which could cause the request to hang indefinitely if Google's API is unresponsive. Consider using AbortController with a timeout.

♻️ Add request timeout
+const REQUEST_TIMEOUT_MS = 30000; // 30 seconds
+
 const makeGoogleApiRequest = async <T>(
   url: string,
   accessToken: string
 ): Promise<GoogleApiResult<T>> => {
+  const controller = new AbortController();
+  const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
+
   try {
     const response = await fetch(url, {
       headers: buildAuthHeader(accessToken),
+      signal: controller.signal,
     });
+    clearTimeout(timeoutId);
 
     // ... rest of the function
   } catch (error) {
+    clearTimeout(timeoutId);
+    if (error instanceof Error && error.name === 'AbortError') {
+      loggers.api.error('Google Calendar API request timed out', error);
+      return { success: false, error: 'Request timed out' };
+    }
     loggers.api.error('Google Calendar API request failed', error as Error);
     return {
       success: false,
       error: error instanceof Error ? error.message : 'Unknown error',
     };
   }
 };

253-300: Consider adding a pagination safety limit.

The do-while loop on lines 278-297 continues while pageToken exists. If the API misbehaves or has an unexpectedly large dataset, this could result in excessive requests. Consider adding a max pages limit as a safeguard.

♻️ Add pagination limit
+const MAX_PAGES = 100; // Safety limit
+
 export const listEvents = async (
   accessToken: string,
   calendarId: string,
   options: {
     timeMin?: Date;
     timeMax?: Date;
     maxResults?: number;
     syncToken?: string;
   } = {}
 ): Promise<GoogleApiResult<{ events: GoogleCalendarEvent[]; nextSyncToken?: string }>> => {
   const events: GoogleCalendarEvent[] = [];
   let pageToken: string | undefined;
   let nextSyncToken: string | undefined;
+  let pageCount = 0;
 
   // ... options setup ...
 
   do {
+    if (++pageCount > MAX_PAGES) {
+      loggers.api.warn('Pagination limit reached for calendar events', { calendarId, pageCount });
+      break;
+    }
     const url = buildEventsListUrl(calendarId, { ...listOptions, pageToken });
     // ... rest of the loop
   } while (pageToken);

Comment thread apps/web/src/app/api/integrations/google-calendar/callback/route.ts
Comment thread apps/web/src/app/api/integrations/google-calendar/connect/route.ts Outdated
Comment thread apps/web/src/app/settings/integrations/google-calendar/page.tsx Outdated
Comment thread apps/web/src/lib/integrations/google-calendar/sync-service.ts
- Remove PII (googleEmail) from auth logs in callback route
- Handle empty/invalid JSON body gracefully in connect route
- Fix useEffect missing dependency with useCallback for handleSync
- Fix per-calendar sync cursor storage (prevents cursor overwrite)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@2witstudios

Copy link
Copy Markdown
Owner Author

Addressed all CodeRabbit review feedback in commit c6fc6d3:

1. Remove PII from auth logs (callback/route.ts:181-184)

  • Removed googleEmail from the auth success log. Only userId is now logged.

2. Handle empty/invalid JSON body gracefully (connect/route.ts:46-51)

  • Wrapped req.json() in try/catch to handle empty or invalid JSON bodies.
  • Falls back to empty object {} if parsing fails, allowing Zod validation to handle properly.

3. Fix useEffect missing dependency (settings page.tsx:60-68)

  • Converted handleSync to useCallback with csrfToken dependency.
  • Added handleSync to the useEffect dependency array.
  • Reordered function declarations to avoid "used before declaration" error.

4. Fix per-calendar sync cursor storage (sync-service.ts:78-106)

  • Changed syncCursor from a single string to a JSON object storing per-calendar cursors.
  • Added parseSyncCursors and serializeSyncCursors helper functions.
  • Each calendar now maintains its own sync token, preventing overwrites.
  • Backward compatible: old single-token format is discarded gracefully on first sync.

- Replace non-existent @/lib/api/fetchWithAuth with @/lib/auth/auth-fetch
- Remove non-existent useCSRF hook - fetchWithAuth handles CSRF automatically
- Follows established patterns from billing and connections pages

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@apps/web/src/app/settings/integrations/google-calendar/page.tsx`:
- Around line 195-244: getStatusBadge currently falls through to the default
"Disconnected" for unknown statuses but the integration type includes "pending"
and "revoked", so update getStatusBadge to explicitly handle
status.connection.status === "pending" and "revoked" (in addition to the
existing "active", "expired", and "error") and return clear Badge variants
(e.g., a neutral/info style for "Pending" and a destructive/revoked style for
"Revoked") so the UI accurately reflects those states; locate the switch in
getStatusBadge and add cases for "pending" and "revoked" using the Badge
component and appropriate icons/text.

Comment thread apps/web/src/app/settings/integrations/google-calendar/page.tsx
- Add pending and revoked to ConnectionStatus type
- Show appropriate badges for all connection status states

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@2witstudios

Copy link
Copy Markdown
Owner Author

Summary of Review Feedback Addressed

All CodeRabbit review comments have been addressed across 4 commits:

Commit c6fc6d3 - Initial Review Fixes

  1. Remove PII from auth logs - Removed googleEmail from callback route logs
  2. Handle empty/invalid JSON body - Added try/catch wrapper in connect route
  3. Fix useEffect dependency - Converted handleSync to useCallback
  4. Fix per-calendar sync cursor - Changed to JSON object storing per-calendar tokens

Commit e478842 - Infrastructure Fix

  1. Use existing fetchWithAuth infrastructure - Replaced non-existent imports with existing @/lib/auth/auth-fetch pattern

Commit 47e148f - Status Badge Handling

  1. Handle pending/revoked statuses - Added explicit badge handling for all connection states

CI Status

  • Unit Tests: ✅ SUCCESS
  • Lint & TypeScript Check: ✅ SUCCESS
  • CodeRabbit: ✅ SUCCESS

All review threads resolved. PR is ready for final review.

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