Files
calendar/packages/app-store/pipedrive-crm/api/callback.ts
T
Anik Dhabal BabuGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
4162b5be55 feat: remove Revert.dev dependency from Pipedrive integration (#22492)
* feat: remove Revert.dev dependency from Pipedrive integration

- Replace Revert API calls with direct Pipedrive OAuth2 and REST API
- Update OAuth flow to use Pipedrive's authorization endpoint
- Implement token exchange in callback handler
- Map Revert's unified API format to Pipedrive's native API structure
- Remove REVERT_* environment variables from configuration
- Update app config to reflect direct Pipedrive integration
- Follow patterns from other CRM integrations like HubSpot

Breaking change: Requires PIPEDRIVE_CLIENT_ID and PIPEDRIVE_CLIENT_SECRET
environment variables instead of REVERT_* variables

Co-Authored-By: anik@cal.com <adhabal2002@gmail.com>

* feat: implement proper token refresh logic for Pipedrive OAuth

- Add token refresh implementation following Pipedrive OAuth2 specification
- Use proper Basic Auth for token refresh requests
- Handle token refresh errors with appropriate logging
- Update access token and expiry date after successful refresh
- Add note about database credential update requirement

Co-Authored-By: anik@cal.com <adhabal2002@gmail.com>

* feat: add database credential updates for Pipedrive token refresh

- Import updateTokenObjectInDb utility for proper credential management
- Store credentialId in constructor for database updates
- Update token refresh logic to persist refreshed tokens to database
- Follow Cal.com patterns for OAuth credential management
- Ensure tokens are properly updated after successful refresh

Co-Authored-By: anik@cal.com <adhabal2002@gmail.com>

* refactor: use OAuthManager for Pipedrive token management

- Refactor CrmService.ts to use OAuthManager instead of manual token refresh
- Create getPipedriveAppKeys helper function for client credentials
- Update callback.ts to use the new helper function
- Add requestPipedrive helper method for all API calls
- Implement proper token refresh callbacks (isTokenObjectUnusable, isAccessTokenUnusable)

This addresses the review comments from hariom and cubic on PR #22492.

Co-Authored-By: anik@cal.com <adhabal2002@gmail.com>

* fix: validate token endpoint response before storing Pipedrive credentials

Co-Authored-By: unknown <>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-21 13:42:15 +05:30

70 lines
2.3 KiB
TypeScript

import type { NextApiRequest, NextApiResponse } from "next";
import { WEBAPP_URL_FOR_OAUTH } from "@calcom/lib/constants";
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
import createOAuthAppCredential from "../../_utils/oauth/createOAuthAppCredential";
import { decodeOAuthState } from "../../_utils/oauth/decodeOAuthState";
import appConfig from "../config.json";
import { getPipedriveAppKeys } from "../lib/getPipedriveAppKeys";
export interface PipedriveToken {
access_token: string;
token_type: string;
refresh_token: string;
scope: string;
expires_in: number;
api_domain: string;
expiryDate?: number;
}
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { code } = req.query;
const state = decodeOAuthState(req);
if (code && typeof code !== "string") {
res.status(400).json({ message: "`code` must be a string" });
return;
}
if (!req.session?.user?.id) {
return res.status(401).json({ message: "You must be logged in to do this" });
}
const { client_id, client_secret } = await getPipedriveAppKeys();
const redirectUri = `${WEBAPP_URL_FOR_OAUTH}/api/integrations/pipedrive-crm/callback`;
const authHeader = `Basic ${Buffer.from(`${client_id}:${client_secret}`).toString("base64")}`;
const tokenResponse = await fetch("https://oauth.pipedrive.com/oauth/token", {
method: "POST",
headers: {
Authorization: authHeader,
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
grant_type: "authorization_code",
code: code || "",
redirect_uri: redirectUri,
}),
});
if (!tokenResponse.ok) {
const errorBody = await tokenResponse.text();
res.status(tokenResponse.status).json({ message: "Failed to exchange OAuth code", detail: errorBody });
return;
}
const pipedriveToken: PipedriveToken = await tokenResponse.json();
pipedriveToken.expiryDate = Math.round(Date.now() + pipedriveToken.expires_in * 1000);
await createOAuthAppCredential({ appId: appConfig.slug, type: appConfig.type }, pipedriveToken, req);
res.redirect(
getSafeRedirectUrl(state?.returnTo) ??
getInstalledAppPath({ variant: appConfig.variant, slug: appConfig.slug })
);
}