-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add Google Drive integration via Composio #103
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why do we need an API endpoint for refreshing the sheets token? |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| import { NextRequest, NextResponse } from "next/server"; | ||
| import { refreshConnectedAccount } from "@/lib/composio/refreshConnectedAccount"; | ||
| import { getCorsHeaders } from "@/lib/networking/getCorsHeaders"; | ||
|
|
||
| /** | ||
| * OPTIONS handler for CORS preflight requests. | ||
| */ | ||
| export async function OPTIONS() { | ||
| return new NextResponse(null, { | ||
| status: 200, | ||
| headers: getCorsHeaders(), | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * POST handler for refreshing a Google Sheets connected account. | ||
| */ | ||
| export async function POST(request: NextRequest) { | ||
| try { | ||
| const body = await request.json(); | ||
| const { accountId, redirectUrl } = body; | ||
|
|
||
| if (!accountId) { | ||
| return NextResponse.json( | ||
| { error: "accountId is required" }, | ||
| { status: 400, headers: getCorsHeaders() }, | ||
| ); | ||
| } | ||
|
Comment on lines
+23
to
+28
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. use
|
||
|
|
||
| const response = await refreshConnectedAccount( | ||
| "GOOGLE_SHEETS", | ||
| accountId, | ||
| redirectUrl, | ||
| ); | ||
|
|
||
| return NextResponse.json( | ||
| { message: "Connected account refreshed successfully", ...response }, | ||
| { status: 200, headers: getCorsHeaders() }, | ||
| ); | ||
| } catch (error) { | ||
| const errorMessage = | ||
| error instanceof Error ? error.message : "Internal server error"; | ||
| const statusCode = errorMessage.includes("not found") ? 404 : 500; | ||
|
|
||
| return NextResponse.json( | ||
| { error: errorMessage }, | ||
| { status: statusCode, headers: getCorsHeaders() }, | ||
| ); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| import { CreateConnectedAccountOptions } from "@composio/core"; | ||
| import { getComposioClient } from "./client"; | ||
| import { ComposioToolkitKey, getToolkitAuthConfigId } from "./toolkits"; | ||
|
|
||
| /** | ||
| * Initiate OAuth authentication for a Composio toolkit. | ||
| * | ||
| * @param toolkitKey - The toolkit to authenticate (e.g., "GOOGLE_SHEETS", "GOOGLE_DRIVE") | ||
| * @param userId - The user ID to authenticate | ||
| * @param options - Optional callback URL and other options | ||
| * @returns The connection request from Composio | ||
| */ | ||
| export async function authenticateToolkit( | ||
| toolkitKey: ComposioToolkitKey, | ||
| userId: string, | ||
| options?: CreateConnectedAccountOptions, | ||
| ) { | ||
| const authConfigId = getToolkitAuthConfigId(toolkitKey); | ||
| const composio = getComposioClient(); | ||
|
|
||
| const connectionRequest = await composio.connectedAccounts.initiate( | ||
| userId, | ||
| authConfigId, | ||
| options, | ||
| ); | ||
|
|
||
| return connectionRequest; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| import { CreateConnectedAccountOptions } from "@composio/core"; | ||
| import { getComposioClient } from "./client"; | ||
| import { authenticateToolkit } from "./authenticateToolkit"; | ||
| import { ComposioToolkitKey, getToolkitConfig } from "./toolkits"; | ||
|
|
||
| /** | ||
| * Get a user's connected account for a Composio toolkit. | ||
| * If no connection exists, initiates the authentication flow. | ||
| * | ||
| * @param toolkitKey - The toolkit to check (e.g., "GOOGLE_SHEETS", "GOOGLE_DRIVE") | ||
| * @param accountId - The user's account ID | ||
| * @param options - Optional callback URL and other options | ||
| * @returns The user's connected accounts for this toolkit | ||
| */ | ||
| export async function getConnectedAccount( | ||
| toolkitKey: ComposioToolkitKey, | ||
| accountId: string, | ||
| options?: CreateConnectedAccountOptions, | ||
| ) { | ||
| const config = getToolkitConfig(toolkitKey); | ||
| const composio = getComposioClient(); | ||
|
|
||
| let userAccounts = await composio.connectedAccounts.list({ | ||
| userIds: [accountId], | ||
| toolkitSlugs: [config.slug], | ||
| }); | ||
|
|
||
| if (userAccounts.items.length === 0) { | ||
| await authenticateToolkit(toolkitKey, accountId, options); | ||
| userAccounts = await composio.connectedAccounts.list({ | ||
| userIds: [accountId], | ||
| toolkitSlugs: [config.slug], | ||
| }); | ||
| } | ||
|
Comment on lines
+28
to
+34
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Logic issue: OAuth flow requires user interaction before retry. After calling Consider one of these approaches:
Proposed approach 1: Return connection request export async function getConnectedAccount(
toolkitKey: ComposioToolkitKey,
accountId: string,
options?: CreateConnectedAccountOptions,
) {
const config = getToolkitConfig(toolkitKey);
const composio = getComposioClient();
let userAccounts = await composio.connectedAccounts.list({
userIds: [accountId],
toolkitSlugs: [config.slug],
});
if (userAccounts.items.length === 0) {
- await authenticateToolkit(toolkitKey, accountId, options);
- userAccounts = await composio.connectedAccounts.list({
- userIds: [accountId],
- toolkitSlugs: [config.slug],
- });
+ const connectionRequest = await authenticateToolkit(toolkitKey, accountId, options);
+ // Return connection request for caller to handle OAuth flow
+ return { items: [], connectionRequest };
}
return userAccounts;
}
🤖 Prompt for AI Agents |
||
|
|
||
| return userAccounts; | ||
| } | ||
This file was deleted.
This file was deleted.
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| import { getConnectedAccount } from "./getConnectedAccount"; | ||
| import { getComposioApiKey } from "./getComposioApiKey"; | ||
| import { ComposioToolkitKey, getToolkitConfig } from "./toolkits"; | ||
|
|
||
| export interface ConnectedAccountRefreshResponse { | ||
| id: string; | ||
| redirect_url: string | null; | ||
| status: | ||
| | "INITIALIZING" | ||
| | "INITIATED" | ||
| | "ACTIVE" | ||
| | "FAILED" | ||
| | "EXPIRED" | ||
| | "INACTIVE"; | ||
| } | ||
|
|
||
| const COMPOSIO_API_BASE_URL = "https://backend.composio.dev"; | ||
|
|
||
| /** | ||
| * Refresh a connected account for a Composio toolkit. | ||
| * | ||
| * @param toolkitKey - The toolkit to refresh (e.g., "GOOGLE_SHEETS", "GOOGLE_DRIVE") | ||
| * @param accountId - The user's account ID | ||
| * @param redirectUrl - The URL to redirect to after OAuth | ||
| * @returns The refresh response with OAuth redirect URL | ||
| */ | ||
| export async function refreshConnectedAccount( | ||
| toolkitKey: ComposioToolkitKey, | ||
| accountId: string, | ||
| redirectUrl?: string, | ||
| ): Promise<ConnectedAccountRefreshResponse | null> { | ||
|
Comment on lines
+27
to
+31
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix return type inconsistency. The function signature declares 🔧 Proposed fix-): Promise<ConnectedAccountRefreshResponse | null> {
+): Promise<ConnectedAccountRefreshResponse> {Also applies to: 71-71 🤖 Prompt for AI Agents |
||
| const apiKey = getComposioApiKey(); | ||
| const config = getToolkitConfig(toolkitKey); | ||
| const accounts = await getConnectedAccount(toolkitKey, accountId); | ||
|
|
||
| if (!accounts.items || accounts.items.length === 0) { | ||
| throw new Error(`${config.name} connected account not found`); | ||
| } | ||
|
|
||
| const connectedAccountId = accounts.items[0].id; | ||
|
|
||
| if (!connectedAccountId) { | ||
| throw new Error(`${config.name} connected account ID not found`); | ||
| } | ||
|
|
||
| const url = new URL( | ||
| `${COMPOSIO_API_BASE_URL}/api/v3/connected_accounts/${connectedAccountId}/refresh`, | ||
| ); | ||
|
|
||
| const body: { redirect_url?: string } = {}; | ||
| if (redirectUrl) { | ||
| body.redirect_url = redirectUrl; | ||
| } | ||
|
|
||
| const response = await fetch(url.toString(), { | ||
| method: "POST", | ||
| headers: { | ||
| "Content-Type": "application/json", | ||
| "x-api-key": apiKey, | ||
| }, | ||
| body: JSON.stringify(body), | ||
| }); | ||
|
|
||
| if (!response.ok) { | ||
| const errorText = await response.text(); | ||
| throw new Error( | ||
| `Failed to refresh ${config.name} connected account: ${response.status} ${errorText}`, | ||
| ); | ||
| } | ||
|
|
||
| return await response.json(); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing error logging in the catch block. The old endpoint logged all errors with
console.error(), but the new implementation doesn't, reducing observability in production.View Details
📝 Patch Details
Analysis
Missing error logging in refresh endpoints
What fails: The POST handlers in
app/api/connectedAccounts/googleDrive/refresh/route.tsandapp/api/connectedAccounts/googleSheets/refresh/route.tsdo not log errors to the server console when refresh operations fail, reducing observability in production.How to reproduce:
Result: When the underlying API call fails, no error is logged to the server console. The error is sent to the client but server-side observability is lost.
Expected behavior: Errors should be logged server-side with
console.error("Error refreshing connected account:", error);matching:app/api/image/generate/route.ts,app/api/x402/image/generate/route.ts)References: