forked from calcom/cal.com
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix: Credential Syncing Improvements (calcom#14588)
* Add example app to test credential sync * Fixes * Changes to normalize flow of GoogleCalendar and Zoom * Add unit tests * PR Feedback * credential-sync-more-tests-and-more-apps * Fix yarn.lock * Clear cache * Add test * Fix yarn.lock * Fix 204 handling * Fix yarn.lock --------- Co-authored-by: Joe Au-Yeung <[email protected]>
- Loading branch information
1 parent
ebbb1dc
commit d47c6b3
Showing
44 changed files
with
3,610 additions
and
593 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
CALCOM_TEST_USER_ID=1 | ||
|
||
GOOGLE_REFRESH_TOKEN= | ||
GOOGLE_CLIENT_ID= | ||
GOOGLE_CLIENT_SECRET= | ||
|
||
ZOOM_REFRESH_TOKEN= | ||
ZOOM_CLIENT_ID= | ||
ZOOM_CLIENT_SECRET= | ||
CALCOM_ADMIN_API_KEY= | ||
|
||
# Refer to Cal.com env variables as these are set in their env | ||
CALCOM_CREDENTIAL_SYNC_SECRET=""; | ||
CALCOM_CREDENTIAL_SYNC_HEADER_NAME="calcom-credential-sync-secret"; | ||
CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY=""; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
# README | ||
|
||
This is an example app that acts as the source of truth for Cal.com Apps credentials. This app is capable of generating the access_token itself and then sync those to Cal.com app. | ||
|
||
## How to start | ||
`yarn dev` starts the server on port 5100. After this open http://localhost:5100 and from there you would be able to manage the tokens for various Apps. | ||
|
||
## Endpoints | ||
http://localhost:5100/api/getToken should be set as the value of env variable CALCOM_CREDENTIAL_SYNC_ENDPOINT in Cal.com |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
// How to get it? -> Establish a connection with Google(e.g. through cal.com app) and then copy the refresh_token from there. | ||
export const GOOGLE_REFRESH_TOKEN = process.env.GOOGLE_REFRESH_TOKEN; | ||
export const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID; | ||
export const GOOGLE_CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET; | ||
|
||
export const ZOOM_REFRESH_TOKEN = process.env.ZOOM_REFRESH_TOKEN; | ||
export const ZOOM_CLIENT_ID = process.env.ZOOM_CLIENT_ID; | ||
export const ZOOM_CLIENT_SECRET = process.env.ZOOM_CLIENT_SECRET; | ||
export const CALCOM_ADMIN_API_KEY = process.env.CALCOM_ADMIN_API_KEY; | ||
|
||
export const CALCOM_CREDENTIAL_SYNC_SECRET = process.env.CALCOM_CREDENTIAL_SYNC_SECRET; | ||
export const CALCOM_CREDENTIAL_SYNC_HEADER_NAME = process.env.CALCOM_CREDENTIAL_SYNC_HEADER_NAME; | ||
export const CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY = process.env.CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
import { | ||
GOOGLE_CLIENT_ID, | ||
GOOGLE_CLIENT_SECRET, | ||
GOOGLE_REFRESH_TOKEN, | ||
ZOOM_CLIENT_ID, | ||
ZOOM_CLIENT_SECRET, | ||
ZOOM_REFRESH_TOKEN, | ||
} from "../constants"; | ||
|
||
export async function generateGoogleCalendarAccessToken() { | ||
const keys = { | ||
client_id: GOOGLE_CLIENT_ID, | ||
client_secret: GOOGLE_CLIENT_SECRET, | ||
redirect_uris: [ | ||
"http://localhost:3000/api/integrations/googlecalendar/callback", | ||
"http://localhost:3000/api/auth/callback/google", | ||
], | ||
}; | ||
const clientId = keys.client_id; | ||
const clientSecret = keys.client_secret; | ||
const refresh_token = GOOGLE_REFRESH_TOKEN; | ||
|
||
const url = "https://oauth2.googleapis.com/token"; | ||
const data = { | ||
client_id: clientId, | ||
client_secret: clientSecret, | ||
refresh_token: refresh_token, | ||
grant_type: "refresh_token", | ||
}; | ||
|
||
try { | ||
const response = await fetch(url, { | ||
method: "POST", | ||
headers: { | ||
"Content-Type": "application/x-www-form-urlencoded", | ||
}, | ||
body: new URLSearchParams(data), | ||
}); | ||
|
||
const json = await response.json(); | ||
if (json.access_token) { | ||
console.log("Access Token:", json.access_token); | ||
return json.access_token; | ||
} else { | ||
console.error("Failed to retrieve access token:", json); | ||
return null; | ||
} | ||
} catch (error) { | ||
console.error("Error fetching access token:", error); | ||
return null; | ||
} | ||
} | ||
|
||
export async function generateZoomAccessToken() { | ||
const client_id = ZOOM_CLIENT_ID; // Replace with your client ID | ||
const client_secret = ZOOM_CLIENT_SECRET; // Replace with your client secret | ||
const refresh_token = ZOOM_REFRESH_TOKEN; // Replace with your refresh token | ||
|
||
const url = "https://zoom.us/oauth/token"; | ||
const auth = Buffer.from(`${client_id}:${client_secret}`).toString("base64"); | ||
|
||
const params = new URLSearchParams(); | ||
params.append("grant_type", "refresh_token"); | ||
params.append("refresh_token", refresh_token); | ||
|
||
try { | ||
const response = await fetch(url, { | ||
method: "POST", | ||
headers: { | ||
Authorization: `Basic ${auth}`, | ||
"Content-Type": "application/x-www-form-urlencoded", | ||
}, | ||
body: params, | ||
}); | ||
|
||
const json = await response.json(); | ||
if (json.access_token) { | ||
console.log("New Access Token:", json.access_token); | ||
console.log("New Refresh Token:", json.refresh_token); // Save this refresh token securely | ||
return json.access_token; // You might also want to return the new refresh token if applicable | ||
} else { | ||
console.error("Failed to refresh access token:", json); | ||
return null; | ||
} | ||
} catch (error) { | ||
console.error("Error refreshing access token:", error); | ||
return null; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
/// <reference types="next" /> | ||
/// <reference types="next/image-types/global" /> | ||
|
||
// NOTE: This file should not be edited | ||
// see https://nextjs.org/docs/basic-features/typescript for more information. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
/** @type {import('next').NextConfig} */ | ||
require("dotenv").config({ path: "../../.env" }); | ||
|
||
const nextConfig = { | ||
reactStrictMode: true, | ||
transpilePackages: ["@calcom/lib"], | ||
}; | ||
|
||
module.exports = nextConfig; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
{ | ||
"name": "@calcom/example-app-credential-sync", | ||
"version": "0.1.0", | ||
"private": true, | ||
"scripts": { | ||
"dev": "PORT=5100 next dev", | ||
"build": "next build", | ||
"start": "next start", | ||
"lint": "next lint" | ||
}, | ||
"dependencies": { | ||
"@calcom/atoms": "*", | ||
"@prisma/client": "5.4.2", | ||
"next": "14.0.4", | ||
"prisma": "^5.7.1", | ||
"react": "^18", | ||
"react-dom": "^18" | ||
}, | ||
"devDependencies": { | ||
"@types/node": "^20.3.1", | ||
"@types/react": "^18", | ||
"@types/react-dom": "^18", | ||
"autoprefixer": "^10.0.1", | ||
"dotenv": "^16.3.1", | ||
"eslint": "^8", | ||
"eslint-config-next": "14.0.4", | ||
"postcss": "^8", | ||
"tailwindcss": "^3.3.0", | ||
"typescript": "^4.9.4" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import type { NextApiRequest, NextApiResponse } from "next"; | ||
|
||
import { CALCOM_CREDENTIAL_SYNC_HEADER_NAME, CALCOM_CREDENTIAL_SYNC_SECRET } from "../../constants"; | ||
import { generateGoogleCalendarAccessToken, generateZoomAccessToken } from "../../lib/integrations"; | ||
|
||
export default async function handler(req: NextApiRequest, res: NextApiResponse) { | ||
const secret = req.headers[CALCOM_CREDENTIAL_SYNC_HEADER_NAME]; | ||
console.log("getToken hit"); | ||
try { | ||
if (!secret) { | ||
return res.status(403).json({ message: "secret header not set" }); | ||
} | ||
if (secret !== CALCOM_CREDENTIAL_SYNC_SECRET) { | ||
return res.status(403).json({ message: "Invalid secret" }); | ||
} | ||
|
||
const calcomUserId = req.body.calcomUserId; | ||
const appSlug = req.body.appSlug; | ||
console.log("getToken Params", { | ||
calcomUserId, | ||
appSlug, | ||
}); | ||
let accessToken; | ||
if (appSlug === "google-calendar") { | ||
accessToken = await generateGoogleCalendarAccessToken(); | ||
} else if (appSlug === "zoom") { | ||
accessToken = await generateZoomAccessToken(); | ||
} else { | ||
throw new Error("Unhandled values"); | ||
} | ||
if (!accessToken) { | ||
throw new Error("Unable to generate token"); | ||
} | ||
res.status(200).json({ | ||
_1: true, | ||
access_token: accessToken, | ||
}); | ||
} catch (e) { | ||
res.status(500).json({ error: e.message }); | ||
} | ||
} |
67 changes: 67 additions & 0 deletions
67
example-apps/credential-sync/pages/api/setTokenInCalCom.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
import type { NextApiRequest } from "next"; | ||
|
||
import { symmetricEncrypt } from "@calcom/lib/crypto"; | ||
|
||
import { | ||
CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY, | ||
CALCOM_CREDENTIAL_SYNC_SECRET, | ||
CALCOM_CREDENTIAL_SYNC_HEADER_NAME, | ||
CALCOM_ADMIN_API_KEY, | ||
} from "../../constants"; | ||
import { generateGoogleCalendarAccessToken, generateZoomAccessToken } from "../../lib/integrations"; | ||
|
||
export default async function handler(req: NextApiRequest, res) { | ||
const isInvalid = req.query["invalid"] === "1"; | ||
const userId = parseInt(req.query["userId"] as string); | ||
const appSlug = req.query["appSlug"]; | ||
|
||
try { | ||
let accessToken; | ||
if (appSlug === "google-calendar") { | ||
accessToken = await generateGoogleCalendarAccessToken(); | ||
} else if (appSlug === "zoom") { | ||
accessToken = await generateZoomAccessToken(); | ||
} else { | ||
throw new Error(`Unhandled appSlug: ${appSlug}`); | ||
} | ||
|
||
if (!accessToken) { | ||
return res.status(500).json({ error: "Could not get access token" }); | ||
} | ||
|
||
const result = await fetch( | ||
`http://localhost:3002/api/v1/credential-sync?apiKey=${CALCOM_ADMIN_API_KEY}&userId=${userId}`, | ||
{ | ||
method: "POST", | ||
headers: { | ||
"Content-Type": "application/json", | ||
[CALCOM_CREDENTIAL_SYNC_HEADER_NAME]: CALCOM_CREDENTIAL_SYNC_SECRET, | ||
}, | ||
body: JSON.stringify({ | ||
appSlug, | ||
encryptedKey: symmetricEncrypt( | ||
JSON.stringify({ | ||
access_token: isInvalid ? "1233231231231" : accessToken, | ||
}), | ||
CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY | ||
), | ||
}), | ||
} | ||
); | ||
|
||
const clonedResult = result.clone(); | ||
try { | ||
if (result.ok) { | ||
const json = await result.json(); | ||
return res.status(200).json(json); | ||
} else { | ||
return res.status(400).json({ error: await clonedResult.text() }); | ||
} | ||
} catch (e) { | ||
return res.status(400).json({ error: await clonedResult.text() }); | ||
} | ||
} catch (error) { | ||
console.error(error); | ||
return res.status(400).json({ message: "Internal Server Error", error: error.message }); | ||
} | ||
} |
Oops, something went wrong.