Skip to content

feat(google-calendar): add backend OAuth2 connect/sync routes - #1804

Open
ionfwsrijan wants to merge 1 commit into
Canopus-Labs:mainfrom
ionfwsrijan:fix/1793-google-calendar-backend
Open

feat(google-calendar): add backend OAuth2 connect/sync routes#1804
ionfwsrijan wants to merge 1 commit into
Canopus-Labs:mainfrom
ionfwsrijan:fix/1793-google-calendar-backend

Conversation

@ionfwsrijan

@ionfwsrijan ionfwsrijan commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Problem

The Interview Prep page ships Google Calendar connect/sync functionality in the frontend, but the backend has no /api/google-calendar/* routes at all. Every calendar action fails:

  • frontend/src/utils/apiPaths.js defines GOOGLE_CALENDAR.CONNECT (GET /api/google-calendar/connect), CALLBACK, STATUS, and EVENTS (POST /api/google-calendar/events).
  • frontend/src/pages/InterviewPrep/InterviewPrep.jsx calls /status on every page load, /connect when the user clicks "Connect your Google Calendar", and /events to sync.
  • Express returns its default 404 HTML body for each of these, so /status logs Calendar status error on every visit, Connect shows "Unable to connect Google Calendar", Sync shows "Unable to sync Google Calendar event", and the calendar banner is permanently stuck in the empty "Connect" state.

Fix

Added the missing backend implementation (multi-file):

  • backend/routes/googleCalendarRoutes.jsGET /connect, GET /callback, GET /status, and POST /events, mounted at /api/google-calendar with generalLimiter and protect on the authed routes. POST /events matches the route the current frontend calls (GOOGLE_CALENDAR.EVENTS).
  • backend/controllers/googleCalendarController.js — OAuth2 flow: connect generates a single-use unguessable state and returns the Google auth URL; callback exchanges the code, stores the refresh token encrypted per user, and redirects back to the app; status reports whether the account is linked (and which email); events refreshes the access token and creates a calendar event for the session.
  • backend/models/GoogleCalendarToken.js — per-user encrypted refresh token storage (user-scoped).
  • backend/models/GoogleCalendarAuthState.js — single-use OAuth state mapping with a 10-minute TTL so the callback (which cannot carry a Bearer header) resolves the connecting user securely.
  • backend/utils/googleCalendar.js — minimal OAuth2 + Calendar API client (auth URL, code exchange, token refresh, user info, event creation) using the already-available axios, scoped to https://www.googleapis.com/auth/calendar.events.
  • backend/utils/encryption.js — AES-256-GCM encryption for the stored Google refresh token.
  • backend/Input_validators/ValidateGoogleCalendar.js — Zod validation for the event payload (title, description, ISO start/end times, reminder minutes).
  • backend/server.js — mounts app.use("/api/google-calendar", generalLimiter, googleCalendarRoutes).
  • backend/config/validateEnv.js and backend/.env.example — document the optional GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_CALENDAR_ENCRYPTION_KEY, and GOOGLE_CALENDAR_CALLBACK_URL env vars; when they are missing the integration reports itself as disabled instead of 404ing.

Files changed

  • backend/server.js
  • backend/routes/googleCalendarRoutes.js
  • backend/controllers/googleCalendarController.js
  • backend/models/GoogleCalendarToken.js
  • backend/models/GoogleCalendarAuthState.js
  • backend/utils/googleCalendar.js
  • backend/utils/encryption.js
  • backend/Input_validators/ValidateGoogleCalendar.js
  • backend/tests/googleCalendarController.unit.test.js
  • backend/config/validateEnv.js
  • backend/.env.example

Testing

  • Added backend/tests/googleCalendarController.unit.test.js (12 tests) covering connect (auth URL + state persistence), callback (code exchange, encrypted token storage, redirects), status (connected/not connected/disabled), and event creation (mapped payload, not-connected rejection, API failure).
  • Ran cd backend && npm test: the new tests and all existing tests pass except the pre-existing jobCache.boundedKeys failures that also fail on clean origin/main and are unrelated to this change.
  • Verified the auth URL builder, encryption round-trip, and the Zod event validator directly with Node.

Closes #1793

Ready to merge.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The backend adds Google Calendar OAuth connection, encrypted refresh-token storage, connection status reporting, and event creation. It validates event requests, mounts protected API routes, supports OAuth callbacks, and adds controller unit tests.

Changes

Google Calendar integration

Layer / File(s) Summary
Configuration, validation, and token contracts
backend/.env.example, backend/config/validateEnv.js, backend/Input_validators/ValidateGoogleCalendar.js, backend/utils/encryption.js, backend/models/GoogleCalendarAuthState.js, backend/models/GoogleCalendarToken.js
Adds Google Calendar environment settings, request validation, AES-256-GCM token encryption, OAuth state expiry, and encrypted refresh-token persistence.
OAuth utility flow
backend/utils/googleCalendar.js
Adds configuration checks, redirect resolution, authorization URL construction, authorization-code exchange, access-token refresh, Google account lookup, and event creation.
Controller and route integration
backend/controllers/googleCalendarController.js, backend/routes/googleCalendarRoutes.js, backend/server.js
Adds OAuth connection and callback handlers, connection status reporting, event creation, protected routes, callback routing, and /api/google-calendar server mounting.
Controller behavior validation
backend/tests/googleCalendarController.unit.test.js
Tests OAuth setup and callback handling, state validation, token persistence, connection status, event mapping, and API failures.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant GoogleCalendarRoutes
  participant GoogleCalendarController
  participant GoogleCalendarToken
  participant Google
  Client->>GoogleCalendarRoutes: Request calendar connection
  GoogleCalendarRoutes->>GoogleCalendarController: Call OAuth handler
  GoogleCalendarController->>GoogleCalendarToken: Store OAuth state
  GoogleCalendarController->>Google: Redirect for authorization
  Google-->>GoogleCalendarController: Return authorization code
  GoogleCalendarController->>Google: Exchange code and retrieve account
  GoogleCalendarController->>GoogleCalendarToken: Encrypt and store refresh token
  Client->>GoogleCalendarRoutes: Submit calendar event
  GoogleCalendarRoutes->>GoogleCalendarController: Validate event request
  GoogleCalendarController->>GoogleCalendarToken: Load refresh token
  GoogleCalendarController->>Google: Refresh access token and create event
  Google-->>Client: Return created event
Loading

Possibly related PRs

Suggested labels: type:feature

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR implements the OAuth flow, token storage, status, event creation, validation, authentication, rate limiting, and route mounting, but uses /events instead of the frontend's required /sync-events path. Use POST /api/google-calendar/sync-events, or update the frontend call site and confirm that the linked issue permits that contract change.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the primary change: backend Google Calendar OAuth2 connection and synchronization routes.
Out of Scope Changes check ✅ Passed The added configuration, models, utilities, controllers, routes, server mounting, encryption, validation, and tests support the linked Google Calendar backend objective.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

}

try {
const authState = await GoogleCalendarAuthState.findOne({ state });

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/controllers/googleCalendarController.js`:
- Around line 53-76: Update the OAuth callback’s auth-state lookup to use
findOneAndDelete with a createdAt cutoff of ten minutes before the current time,
so expired or already-consumed states redirect via the existing !authState path
before token exchange. Remove the later GoogleCalendarAuthState.deleteOne call,
and add coverage for expired states and repeated callbacks.
- Around line 46-53: Update the OAuth callback validation before
GoogleCalendarAuthState.findOne and googleCalendar.exchangeCode to require both
code and state to be strings, rejecting arrays and other non-string values with
the existing frontend redirect. Preserve the current handling for missing
parameters and the valid scalar-string flow.

In `@backend/Input_validators/ValidateGoogleCalendar.js`:
- Around line 4-10: Update createGoogleCalendarEventSchema with an object-level
refinement that requires endTime to be strictly later than startTime, assigning
failures to path ["endTime"]. Preserve the existing timestamp validation and
return a clear validation message so invalid durations are rejected by
middleware as HTTP 400.

In `@backend/utils/googleCalendar.js`:
- Line 9: Update the scope definition used by googleCalendarCallback to include
openid and email alongside the existing calendar.events scope, ensuring the
generated authorization URL requests the UserInfo identity scopes; add or update
coverage to verify all required scopes appear in that URL.
- Around line 42-85: Configure a shared Axios client with a bounded timeout and
use it for the requests in exchangeCode, refreshAccessToken, getGoogleUserInfo,
and createCalendarEvent. Preserve each request’s existing URL, payload, and
headers, and do not add automatic retries to createCalendarEvent.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 697448b6-ac73-4f64-abf0-f9af212f0bc0

📥 Commits

Reviewing files that changed from the base of the PR and between 7d3f898 and 8ad0c8b.

📒 Files selected for processing (11)
  • backend/.env.example
  • backend/Input_validators/ValidateGoogleCalendar.js
  • backend/config/validateEnv.js
  • backend/controllers/googleCalendarController.js
  • backend/models/GoogleCalendarAuthState.js
  • backend/models/GoogleCalendarToken.js
  • backend/routes/googleCalendarRoutes.js
  • backend/server.js
  • backend/tests/googleCalendarController.unit.test.js
  • backend/utils/encryption.js
  • backend/utils/googleCalendar.js

Comment on lines +46 to +53
const { code, state } = req.query;

if (!code || !state) {
return res.redirect(FRONTEND_URL);
}

try {
const authState = await GoogleCalendarAuthState.findOne({ state });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect whether the application changes Express query parsing.
rg -n -C 3 'query parser|app\.set\s*\(' backend

# Locate callback tests that should cover a non-string state value.
rg -n -C 3 'googleCalendarCallback|state:' backend/tests

Repository: Canopus-Labs/PrepPilot

Length of output: 4292


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- controller ---'
sed -n '1,130p' backend/controllers/googleCalendarController.js

printf '%s\n' '--- server query-related setup ---'
sed -n '1,100p' backend/server.js

printf '%s\n' '--- package versions ---'
rg -n '"express"|"mongoose"|"qs"|"querystring"' package.json backend/package.json 2>/dev/null || true

printf '%s\n' '--- relevant tests ---'
sed -n '90,180p' backend/tests/googleCalendarController.unit.test.js

Repository: Canopus-Labs/PrepPilot

Length of output: 10882


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- auth-state schema ---'
fd -i 'GoogleCalendarAuthState' . --type f -x sh -c 'echo "--- $1"; cat -n "$1"' sh {}

printf '%s\n' '--- all query-parser configuration and req.query handling ---'
rg -n -C 3 'query[[:space:]]*parser|req\.query|querystring|qs|express\(' backend

printf '%s\n' '--- Express default parser behavior for relevant inputs ---'
node - <<'JS'
const querystring = require('node:querystring');
for (const input of [
  'state%5B%24ne%5D=x&code=oauth-code',
  'state=x&state=y&code=oauth-code',
  'state=x&code=a&code=b',
  'state=state-123&code=oauth-code',
]) {
  console.log(input, '=>', JSON.stringify(querystring.parse(input)));
}
JS

Repository: Canopus-Labs/PrepPilot

Length of output: 13439


🌐 Web query:

Mongoose 8 String schema query casting array value findOne operator injection official documentation

💡 Result:

In Mongoose 8, schema casting is a primary, built-in defense against operator injection, but it is not a complete solution on its own [1]. When you pass a value to a Mongoose query (like Model.findOne()), Mongoose attempts to cast that value to the type defined in your schema [2][3]. If a schema field is strictly typed (e.g., type: String or type: Number), Mongoose will attempt to cast the input to that type [3][1]. If an attacker provides an object containing MongoDB operators—such as { "$ne": "" }—instead of the expected primitive, casting will typically fail or result in a type-mismatched value, effectively neutralizing the injected operator [1]. However, this protection has critical limitations: 1. Schema-less or 'Mixed' Types: If a schema field is defined as Schema.Types.Mixed or is not explicitly typed, Mongoose may not enforce the expected type, allowing operator objects to pass through to MongoDB [1]. 2. Key-Side Injection: Schema casting validates values, not keys [1]. If your code dynamically constructs query filters using user-supplied keys (e.g., Model.findOne({ [req.body.field]: req.body.value })), an attacker can inject operators into the query structure itself regardless of value-side casting [1]. 3. Bypass of Sanitization: Mongoose provides a sanitizeFilter utility intended to strip operators from queries, but this mechanism has had vulnerabilities. For instance, flaws were identified where operators nested within $nor clauses could bypass sanitization [4]. 4. $where Operator Vulnerabilities: Mongoose has been subject to CVEs (e.g., CVE-2024-53900 and CVE-2025-23061) related to the improper use of the $where operator, which allows execution of arbitrary JavaScript within MongoDB queries [5][6][7]. Always ensure you are using a patched version of Mongoose (at least 8.9.5 or later) [6][7]. To effectively prevent operator injection: - Always validate and sanitize user input before it reaches your database layer [8][1]. - Use explicit allowlists for fields used in queries [8]. - Avoid passing raw request bodies (like req.body or req.query) directly into Mongoose methods [4][8]. - Use tools like Zod or Joi to enforce strict input schemas before the data reaches Mongoose [1]. - Avoid the $where operator entirely [5][7].

Citations:


Validate OAuth callback parameters as strings.

The default Express parser does not convert state[$ne]=x into a nested object. Repeated code or state parameters still produce arrays, and the current truthiness check accepts them. Reject non-string values before GoogleCalendarAuthState.findOne and googleCalendar.exchangeCode.

🧰 Tools
🪛 GitHub Check: CodeQL

[failure] 53-53: Database query built from user-controlled sources
This query object depends on a user-provided value.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/controllers/googleCalendarController.js` around lines 46 - 53, Update
the OAuth callback validation before GoogleCalendarAuthState.findOne and
googleCalendar.exchangeCode to require both code and state to be strings,
rejecting arrays and other non-string values with the existing frontend
redirect. Preserve the current handling for missing parameters and the valid
scalar-string flow.

Source: Linters/SAST tools

Comment on lines +53 to +76
const authState = await GoogleCalendarAuthState.findOne({ state });
if (!authState) {
return res.redirect(FRONTEND_URL);
}

const redirectUri = googleCalendar.getRedirectUri(req);
const tokens = await googleCalendar.exchangeCode(code, redirectUri);
if (!tokens.refresh_token) {
return res.redirect(FRONTEND_URL);
}

const userInfo = await googleCalendar.getGoogleUserInfo(tokens.access_token);

await GoogleCalendarToken.findOneAndUpdate(
{ userId: authState.userId },
{
userId: authState.userId,
refreshTokenEnc: encrypt(tokens.refresh_token),
email: userInfo.email || "",
},
{ upsert: true, new: true },
);

await GoogleCalendarAuthState.deleteOne({ _id: authState._id });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- controller ---'
cat -n backend/controllers/googleCalendarController.js | sed -n '1,130p'

printf '%s\n' '--- auth-state model and indexes ---'
rg -n -S --glob '!node_modules' 'GoogleCalendarAuthState|createdAt|expires|TTL|expireAfter' backend models . | head -200

printf '%s\n' '--- query-parser and callback wiring ---'
rg -n -S --glob '!node_modules' 'query parser|queryParser|extended|googleCalendarController|callback|calendar' backend app.js server.js index.js 2>/dev/null | head -250

printf '%s\n' '--- relevant tests ---'
rg -n -S --glob '!node_modules' 'GoogleCalendar|google calendar|auth state|exchangeCode|getGoogleUserInfo|findOneAndDelete' . | head -250

Repository: Canopus-Labs/PrepPilot

Length of output: 27502


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- auth-state model ---'
cat -n backend/models/GoogleCalendarAuthState.js

printf '%s\n' '--- server parser configuration ---'
cat -n backend/server.js | sed -n '1,210p'

printf '%s\n' '--- callback tests ---'
cat -n backend/tests/googleCalendarController.unit.test.js | sed -n '75,175p'

printf '%s\n' '--- package versions ---'
cat backend/package.json 2>/dev/null | sed -n '1,180p' || true
rg -n '"mongoose"|"express"' package.json backend/package.json package-lock.json backend/package-lock.json 2>/dev/null | head -40

Repository: Canopus-Labs/PrepPilot

Length of output: 15360


🏁 Script executed:

#!/bin/bash
set -eu

python3 - <<'PY'
from pathlib import Path
import re

controller = Path("backend/controllers/googleCalendarController.js").read_text()
model = Path("backend/models/GoogleCalendarAuthState.js").read_text()

find_one = controller.index("GoogleCalendarAuthState.findOne({ state })")
exchange = controller.index("googleCalendar.exchangeCode")
user_info = controller.index("googleCalendar.getGoogleUserInfo")
persist = controller.index("GoogleCalendarToken.findOneAndUpdate")
delete_one = controller.index("GoogleCalendarAuthState.deleteOne")

ttl = re.search(r"createdAt:\s*\{.*?expires:\s*(\d+)", model, re.S)
print({
    "lookup_is_non_consuming_findOne": "findOne({ state })" in controller,
    "delete_after_exchange": delete_one > exchange,
    "delete_after_user_info": delete_one > user_info,
    "delete_after_persistence": delete_one > persist,
    "ttl_seconds": int(ttl.group(1)) if ttl else None,
    "state_deleted_before_exchange": delete_one < exchange,
})
PY

Repository: Canopus-Labs/PrepPilot

Length of output: 359


Consume OAuth state atomically and enforce its expiry in the query.

A TTL index does not enforce request-time expiry. This findOne leaves the state reusable until deletion and when processing fails.

Use findOneAndDelete with a ten-minute createdAt cutoff before the token exchange. Remove the later deleteOne call. Add tests for expired and repeated callbacks.

🧰 Tools
🪛 GitHub Check: CodeQL

[failure] 53-53: Database query built from user-controlled sources
This query object depends on a user-provided value.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/controllers/googleCalendarController.js` around lines 53 - 76, Update
the OAuth callback’s auth-state lookup to use findOneAndDelete with a createdAt
cutoff of ten minutes before the current time, so expired or already-consumed
states redirect via the existing !authState path before token exchange. Remove
the later GoogleCalendarAuthState.deleteOne call, and add coverage for expired
states and repeated callbacks.

Comment on lines +4 to +10
const createGoogleCalendarEventSchema = z.object({
title: z.string().trim().min(1, "Title is required").max(200),
description: z.string().trim().max(2000).optional().default(""),
startTime: z.string().datetime({ offset: true }),
endTime: z.string().datetime({ offset: true }),
reminderMinutes: z.number().int().min(0).max(60).optional().default(15),
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- candidate files ---'
git ls-files | grep -E '(^|/)(ValidateGoogleCalendar\.js|package\.json|package-lock\.json|yarn\.lock|pnpm-lock\.yaml)$' || true

printf '%s\n' '--- validator ---'
file=$(git ls-files | grep 'backend/Input_validators/ValidateGoogleCalendar\.js$' | head -n1)
test -n "$file"
cat -n "$file"

printf '%s\n' '--- relevant usages ---'
rg -n -C 5 'createGoogleCalendarEventSchema|ValidateGoogleCalendar|startTime|endTime' backend --glob '*.js' --glob '*.ts' || true

printf '%s\n' '--- dependency declarations ---'
for f in $(git ls-files | grep -E '(^|/)(package\.json|package-lock\.json|yarn\.lock|pnpm-lock\.yaml)$'); do
  echo "### $f"
  rg -n -C 2 '"zod"|zod@' "$f" || true
done

Repository: Canopus-Labs/PrepPilot

Length of output: 8110


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- controller error path ---'
cat -n backend/controllers/googleCalendarController.js | sed -n '120,190p'

printf '%s\n' '--- validation error helper ---'
cat -n backend/Input_validators/ValidateQuestions.js | sed -n '1,110p'

printf '%s\n' '--- route middleware order ---'
cat -n backend/routes/googleCalendarRoutes.js

printf '%s\n' '--- resolved Zod version ---'
python3 - <<'PY'
import json
from pathlib import Path
p = Path("backend/package-lock.json")
data = json.loads(p.read_text())
for key, value in data.get("packages", {}).items():
    if key.endswith("node_modules/zod") or key == "node_modules/zod":
        print(key, value.get("version"))
PY

printf '%s\n' '--- relevant tests ---'
rg -n -C 8 '500|invalid|validation|startTime|endTime|createGoogleCalendarEvent' backend/tests --glob '*.js' | head -n 240

printf '%s\n' '--- Date ordering probe ---'
node - <<'JS'
const cases = [
  ["2026-08-11T10:00:00.000Z", "2026-08-11T11:00:00.000Z"],
  ["2026-08-11T10:00:00.000Z", "2026-08-11T10:00:00.000Z"],
  ["2026-08-11T11:00:00.000Z", "2026-08-11T10:00:00.000Z"],
  ["2026-08-11T10:00:00+02:00", "2026-08-11T09:00:00Z"],
];
for (const [startTime, endTime] of cases) {
  console.log({startTime, endTime, startMs: Date.parse(startTime), endMs: Date.parse(endTime), valid: new Date(endTime) > new Date(startTime)});
}
JS

Repository: Canopus-Labs/PrepPilot

Length of output: 28412


🌐 Web query:

Zod 4 refine object schema error path option official documentation

💡 Result:

In Zod, you can customize the error path of a refinement by passing a configuration object as the second argument to the.refine method [1][2][3]. This object allows you to specify a path option, which is an array of strings or numbers representing the location of the error [2][3][4]. This feature is primarily used with object schemas to associate a cross-field validation error with a specific property, making it easier to target that field in your UI [1][5][4]. Example usage: const passwordForm = z.object({ password: z.string, confirm: z.string, }).refine((data) => data.password === data.confirm, { message: "Passwords don't match", path: ["confirm"], // The error will be associated with the 'confirm' field }); When validation fails, the resulting ZodError will include this path in its issues array [1][2][3]: { "code": "custom", "path": ["confirm"], "message": "Passwords don't match" } The configuration object passed to.refine accepts the following optional parameters: - message: A string to override the default error message [2][3]. - path: An array (string[] | number[]) to append to the error path [2][3]. - params: An object that can be used to customize the message within an error map [2][3].

Citations:


🌐 Web query:

Google Calendar API events.insert end time must be after start time official documentation

💡 Result:

Yes, when using the Google Calendar API events.insert method, the event's end time must be after its start time [1][2]. The Google Calendar API requires both start and end times to be provided as objects [3][4]. If the end time is equal to or earlier than the start time, the API will reject the request with an error [1][5][2]. Common errors encountered when this condition is not met include a 400 Bad Request response containing a "timeRangeEmpty" reason and a message stating, "The specified time range is empty" [2][6]. To ensure your event is accepted, verify that your start and end date-time strings follow the RFC3339 format (e.g., 2026-08-11T10:00:00Z) and that the start value chronologically precedes the end value [1][7][6].

Citations:


Reject an endTime that is not after startTime.

Both timestamps can be valid while defining an empty or negative event duration. The controller maps the Calendar API failure to HTTP 500. Add an object-level refinement with path: ["endTime"] so the middleware returns HTTP 400.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/Input_validators/ValidateGoogleCalendar.js` around lines 4 - 10,
Update createGoogleCalendarEventSchema with an object-level refinement that
requires endTime to be strictly later than startTime, assigning failures to path
["endTime"]. Preserve the existing timestamp validation and return a clear
validation message so invalid durations are rejected by middleware as HTTP 400.

const GOOGLE_CALENDAR_EVENTS_URL =
"https://www.googleapis.com/calendar/v3/calendars/primary/events";

const GOOGLE_CALENDAR_SCOPE = "https://www.googleapis.com/auth/calendar.events";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- candidate files ---'
fd -i 'googleCalendar|calendar' . | head -80

printf '%s\n' '--- relevant symbols and calls ---'
rg -n -C 8 'GOOGLE_CALENDAR_SCOPE|googleCalendarCallback|UserInfo|userinfo|buildAuthUrl|axios\.(get|post)|timeout' . \
  -g '!node_modules' -g '!dist' -g '!build'

printf '%s\n' '--- file map ---'
if command -v ast-grep >/dev/null 2>&1; then
  ast-grep outline backend/utils/googleCalendar.js --view compact || true
fi

Repository: Canopus-Labs/PrepPilot

Length of output: 41048


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- backend/utils/googleCalendar.js ---'
cat -n backend/utils/googleCalendar.js

printf '%s\n' '--- callback implementation ---'
sed -n '45,90p' backend/controllers/googleCalendarController.js

printf '%s\n' '--- existing calendar tests ---'
sed -n '1,180p' backend/tests/googleCalendarController.unit.test.js

printf '%s\n' '--- behavioral scope probe ---'
python3 - <<'PY'
from pathlib import Path
from urllib.parse import parse_qs, urlparse

source = Path("backend/utils/googleCalendar.js").read_text()
scope_line = next(
    line.strip()
    for line in source.splitlines()
    if line.startswith("const GOOGLE_CALENDAR_SCOPE =")
)
scope = scope_line.split("=", 1)[1].strip().rstrip(";").strip('"')
url = "https://accounts.google.com/o/oauth2/v2/auth?" + \
      "scope=" + __import__("urllib.parse").parse.quote(scope, safe="")
parsed_scope = parse_qs(urlparse(url).query)["scope"][0]
print("scope_constant:", scope)
print("authorization_scope:", parsed_scope)
print("scope_tokens:", parsed_scope.split())
print("has_openid:", "openid" in parsed_scope.split())
print("has_email:", "email" in parsed_scope.split())
print("userinfo_call_has_timeout:", "timeout:" in source[source.index("const getGoogleUserInfo"):source.index("const createCalendarEvent")])
PY

Repository: Canopus-Labs/PrepPilot

Length of output: 11333


🌐 Web query:

Google OpenID Connect UserInfo endpoint email claim openid email scope official documentation

💡 Result:

To receive the email claim from the Google OpenID Connect (OIDC) UserInfo endpoint, you must include the email scope in your authentication request [1][2]. Official Google documentation specifies the following regarding the email claim and scope: 1. Requirement: The email claim is only provided if the email scope is included in the authorization request [1][2]. 2. Endpoint: The UserInfo endpoint returns user information in a JSON object [1]. To locate the correct endpoint URL, you should use the userinfo_endpoint metadata value retrieved from Google's OpenID Connect discovery document [2]. 3. Claim Behavior: - The email claim contains the user's email address [1]. - If the email scope is present, Google will also provide an email_verified claim, which is a boolean indicating whether the email address has been verified [1]. 4. Important Recommendations: - Google explicitly warns against using the email claim as a primary identifier for a user, as a Google Account's email address can change over time [1][2]. - You should always use the sub (subject) field as the unique, immutable identifier for the user [1][2]. When making a request, your scope parameter must be a space-delimited list that includes openid and at least one other scope (such as email or profile) [1]. Accessing the UserInfo endpoint requires providing a valid access token in the authorization header [2].

Citations:


Request the identity scopes required by the UserInfo call.

googleCalendarCallback persists userInfo.email, but the authorization request includes only calendar.events. Add openid email and test the generated authorization URL.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/utils/googleCalendar.js` at line 9, Update the scope definition used
by googleCalendarCallback to include openid and email alongside the existing
calendar.events scope, ensuring the generated authorization URL requests the
UserInfo identity scopes; add or update coverage to verify all required scopes
appear in that URL.

Comment on lines +42 to +85
const exchangeCode = async (code, redirectUri) => {
const { data } = await axios.post(
GOOGLE_TOKEN_URL,
new URLSearchParams({
code,
client_id: process.env.GOOGLE_CLIENT_ID,
client_secret: process.env.GOOGLE_CLIENT_SECRET,
redirect_uri: redirectUri,
grant_type: "authorization_code",
}),
{ headers: { "Content-Type": "application/x-www-form-urlencoded" } },
);
return data;
};

const refreshAccessToken = async (refreshToken) => {
const { data } = await axios.post(
GOOGLE_TOKEN_URL,
new URLSearchParams({
refresh_token: refreshToken,
client_id: process.env.GOOGLE_CLIENT_ID,
client_secret: process.env.GOOGLE_CLIENT_SECRET,
grant_type: "refresh_token",
}),
{ headers: { "Content-Type": "application/x-www-form-urlencoded" } },
);
return data.access_token;
};

const getGoogleUserInfo = async (accessToken) => {
const { data } = await axios.get(GOOGLE_USERINFO_URL, {
headers: { Authorization: `Bearer ${accessToken}` },
});
return data;
};

const createCalendarEvent = async (accessToken, event) => {
const { data } = await axios.post(GOOGLE_CALENDAR_EVENTS_URL, event, {
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
});
return data;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- target file outline ---'
if command -v ast-grep >/dev/null 2>&1; then
  ast-grep outline backend/utils/googleCalendar.js || true
fi

printf '%s\n' '--- target file size ---'
wc -l backend/utils/googleCalendar.js

printf '%s\n' '--- target file ---'
cat -n backend/utils/googleCalendar.js

printf '%s\n' '--- axios timeout/configuration references ---'
rg -n -S 'axios|timeout|GOOGLE_TOKEN_URL|GOOGLE_USERINFO_URL|GOOGLE_CALENDAR_EVENTS_URL' \
  backend package.json package-lock.json yarn.lock pnpm-lock.yaml 2>/dev/null || true

Repository: Canopus-Labs/PrepPilot

Length of output: 8607


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- Google Calendar helper usages ---'
rg -n -S 'exchangeCode|refreshAccessToken|getGoogleUserInfo|createCalendarEvent|googleCalendar' \
  backend --glob '!backend/package-lock.json'

printf '%s\n' '--- Axios global/default timeout configuration ---'
rg -n -S 'axios\.defaults|create\(\s*\{[^}]*timeout|timeout\s*:' . \
  --glob '!node_modules/**' --glob '!backend/package-lock.json' --glob '!package-lock.json' \
  || true

printf '%s\n' '--- likely controller request paths ---'
rg -n -S 'google-calendar|exchangeCode|refreshAccessToken|getGoogleUserInfo|createCalendarEvent' \
  backend/controllers backend/routes backend --glob '!backend/package-lock.json' \
  | head -200

Repository: Canopus-Labs/PrepPilot

Length of output: 9797


🏁 Script executed:

#!/bin/bash
set -eu

python3 - <<'PY'
from pathlib import Path
import re

path = Path("backend/utils/googleCalendar.js")
source = path.read_text()

calls = list(re.finditer(r"\baxios\.(get|post)\s*\(", source))
print(f"axios_http_call_count={len(calls)}")
for match in calls:
    line = source.count("\n", 0, match.start()) + 1
    end = source.find(");", match.start())
    request = source[match.start(): end if end != -1 else len(source)]
    print(f"line={line} method={match.group(1)} has_timeout={bool(re.search(r'\\btimeout\\s*:', request))}")

print(f"axios_default_assignment_count={len(re.findall(r'axios\\.defaults', source))}")
PY

printf '%s\n' '--- controller callback and event paths ---'
cat -n backend/controllers/googleCalendarController.js | sed -n '42,180p'

Repository: Canopus-Labs/PrepPilot

Length of output: 469


🏁 Script executed:

#!/bin/bash
set -eu

python3 - <<'PY'
from pathlib import Path
import re

path = Path("backend/utils/googleCalendar.js")
source = path.read_text()

calls = list(re.finditer(r"\baxios\.(get|post)\s*\(", source))
print(f"axios_http_call_count={len(calls)}")
for match in calls:
    line = source.count("\n", 0, match.start()) + 1
    end = source.find(");", match.start())
    request = source[match.start(): end if end != -1 else len(source)]
    has_timeout = bool(re.search(r"\btimeout\s*:", request))
    print(f"line={line} method={match.group(1)} has_timeout={has_timeout}")

print(f"axios_default_assignment_count={len(re.findall(r'axios\.defaults', source))}")
PY

printf '%s\n' '--- controller callback and event paths ---'
cat -n backend/controllers/googleCalendarController.js | sed -n '42,180p'

Repository: Canopus-Labs/PrepPilot

Length of output: 433


🏁 Script executed:

#!/bin/bash
set -eu

python3 - <<'PY'
from pathlib import Path
import re

path = Path("backend/utils/googleCalendar.js")
source = path.read_text()

calls = list(re.finditer(r"\baxios\.(get|post)\s*\(", source))
default_assignments = len(re.findall(r"axios\.defaults", source))

print("axios_http_call_count=" + str(len(calls)))
for match in calls:
    line = source.count("\n", 0, match.start()) + 1
    end = source.find(");", match.start())
    request = source[match.start(): end if end != -1 else len(source)]
    has_timeout = bool(re.search(r"\btimeout\s*:", request))
    print(
        "line=" + str(line)
        + " method=" + match.group(1)
        + " has_timeout=" + str(has_timeout)
    )

print("axios_default_assignment_count=" + str(default_assignments))
PY

printf '%s\n' '--- controller callback and event paths ---'
cat -n backend/controllers/googleCalendarController.js | sed -n '42,180p'

Repository: Canopus-Labs/PrepPilot

Length of output: 5051


Set a bounded timeout for all four Google HTTP requests.

None of the four requests has a timeout. A stalled Google request can keep the OAuth callback or event-creation request open indefinitely. Use a configured Axios client with a bounded timeout. Do not automatically retry createCalendarEvent because the operation is non-idempotent.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/utils/googleCalendar.js` around lines 42 - 85, Configure a shared
Axios client with a bounded timeout and use it for the requests in exchangeCode,
refreshAccessToken, getGoogleUserInfo, and createCalendarEvent. Preserve each
request’s existing URL, payload, and headers, and do not add automatic retries
to createCalendarEvent.

@github-actions github-actions Bot added the merge ready PR is mergeable and has no conflicts label Aug 11, 2026
@KaranUnique

Copy link
Copy Markdown
Contributor

@ionfwsrijan Address coderabbit and github action bot suggestions

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge ready PR is mergeable and has no conflicts

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Google Calendar connect/sync in Interview Prep is dead on arrival - frontend calls /api/google-calendar/* routes the backend never mounts

3 participants