This repository has been archived by the owner on Jan 23, 2024. It is now read-only.
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.
feat: OAuth provider for Zapier (calcom#11465)
Co-authored-by: Alex van Andel <[email protected]> Co-authored-by: sajanlamsal <[email protected]> Co-authored-by: CarinaWolli <[email protected]> Co-authored-by: alannnc <[email protected]> Co-authored-by: Leo Giovanetti <[email protected]> Co-authored-by: Peer Richelsen <[email protected]> Co-authored-by: Hariom Balhara <[email protected]> Co-authored-by: Udit Takkar <[email protected]> Co-authored-by: Nitin Panghal <[email protected]> Co-authored-by: Omar López <[email protected]> Co-authored-by: Peer Richelsen <[email protected]> Co-authored-by: zomars <[email protected]> Co-authored-by: Shivam Kalra <[email protected]> Co-authored-by: Richard Poelderl <[email protected]> Co-authored-by: Crowdin Bot <[email protected]> Co-authored-by: Joe Au-Yeung <[email protected]> Co-authored-by: Nafees Nazik <[email protected]> Co-authored-by: Chiranjeev Vishnoi <[email protected]> Co-authored-by: Denzil Samuel <[email protected]> Co-authored-by: Syed Ali Shahbaz <[email protected]> Co-authored-by: nitinpanghal <[email protected]> Co-authored-by: Ahmad <[email protected]> Co-authored-by: Annlee Fores <[email protected]> Co-authored-by: Keith Williams <[email protected]> Co-authored-by: Vijay <[email protected]>
- Loading branch information
1 parent
b4f44e9
commit 68bd877
Showing
31 changed files
with
1,896 additions
and
196 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import type { NextApiRequest, NextApiResponse } from "next"; | ||
|
||
import isAuthorized from "@calcom/features/auth/lib/oAuthAuthorization"; | ||
|
||
export default async function handler(req: NextApiRequest, res: NextApiResponse) { | ||
const requriedScopes = ["READ_PROFILE"]; | ||
|
||
const account = await isAuthorized(req, requriedScopes); | ||
|
||
if (!account) { | ||
return res.status(401).json({ message: "Unauthorized" }); | ||
} | ||
return res.status(201).json({ username: account.name }); | ||
} |
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,68 @@ | ||
import jwt from "jsonwebtoken"; | ||
import type { NextApiRequest, NextApiResponse } from "next"; | ||
import type { OAuthTokenPayload } from "pages/api/auth/oauth/token"; | ||
|
||
import prisma from "@calcom/prisma"; | ||
import { generateSecret } from "@calcom/trpc/server/routers/viewer/oAuth/addClient.handler"; | ||
|
||
export default async function handler(req: NextApiRequest, res: NextApiResponse) { | ||
if (req.method !== "POST") { | ||
res.status(405).json({ message: "Invalid method" }); | ||
return; | ||
} | ||
|
||
const refreshToken = req.headers.authorization?.split(" ")[1] || ""; | ||
|
||
const { client_id, client_secret, grant_type } = req.body; | ||
|
||
if (grant_type !== "refresh_token") { | ||
res.status(400).json({ message: "grant type invalid" }); | ||
return; | ||
} | ||
|
||
const [hashedSecret] = generateSecret(client_secret); | ||
|
||
const client = await prisma.oAuthClient.findFirst({ | ||
where: { | ||
clientId: client_id, | ||
clientSecret: hashedSecret, | ||
}, | ||
select: { | ||
redirectUri: true, | ||
}, | ||
}); | ||
|
||
if (!client) { | ||
res.status(401).json({ message: "Unauthorized" }); | ||
return; | ||
} | ||
|
||
const secretKey = process.env.CALENDSO_ENCRYPTION_KEY || ""; | ||
|
||
let decodedRefreshToken: OAuthTokenPayload; | ||
|
||
try { | ||
decodedRefreshToken = jwt.verify(refreshToken, secretKey) as OAuthTokenPayload; | ||
} catch { | ||
res.status(401).json({ message: "Unauthorized" }); | ||
return; | ||
} | ||
|
||
if (!decodedRefreshToken || decodedRefreshToken.token_type !== "Refresh Token") { | ||
res.status(401).json({ message: "Unauthorized" }); | ||
return; | ||
} | ||
|
||
const payload: OAuthTokenPayload = { | ||
userId: decodedRefreshToken.userId, | ||
scope: decodedRefreshToken.scope, | ||
token_type: "Access Token", | ||
clientId: client_id, | ||
}; | ||
|
||
const access_token = jwt.sign(payload, secretKey, { | ||
expiresIn: 1800, // 30 min | ||
}); | ||
|
||
res.status(200).json({ access_token }); | ||
} |
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,104 @@ | ||
import jwt from "jsonwebtoken"; | ||
import type { NextApiRequest, NextApiResponse } from "next"; | ||
|
||
import prisma from "@calcom/prisma"; | ||
import { generateSecret } from "@calcom/trpc/server/routers/viewer/oAuth/addClient.handler"; | ||
|
||
export type OAuthTokenPayload = { | ||
userId?: number | null; | ||
teamId?: number | null; | ||
token_type: string; | ||
scope: string[]; | ||
clientId: string; | ||
}; | ||
|
||
export default async function handler(req: NextApiRequest, res: NextApiResponse) { | ||
if (req.method !== "POST") { | ||
res.status(405).json({ message: "Invalid method" }); | ||
return; | ||
} | ||
|
||
const { code, client_id, client_secret, grant_type, redirect_uri } = req.body; | ||
|
||
if (grant_type !== "authorization_code") { | ||
res.status(400).json({ message: "grant_type invalid" }); | ||
return; | ||
} | ||
|
||
const [hashedSecret] = generateSecret(client_secret); | ||
|
||
const client = await prisma.oAuthClient.findFirst({ | ||
where: { | ||
clientId: client_id, | ||
clientSecret: hashedSecret, | ||
}, | ||
select: { | ||
redirectUri: true, | ||
}, | ||
}); | ||
|
||
if (!client || client.redirectUri !== redirect_uri) { | ||
res.status(401).json({ message: "Unauthorized" }); | ||
return; | ||
} | ||
|
||
const accessCode = await prisma.accessCode.findFirst({ | ||
where: { | ||
code: code, | ||
clientId: client_id, | ||
expiresAt: { | ||
gt: new Date(), | ||
}, | ||
}, | ||
}); | ||
|
||
//delete all expired accessCodes + the one that is used here | ||
await prisma.accessCode.deleteMany({ | ||
where: { | ||
OR: [ | ||
{ | ||
expiresAt: { | ||
lt: new Date(), | ||
}, | ||
}, | ||
{ | ||
code: code, | ||
clientId: client_id, | ||
}, | ||
], | ||
}, | ||
}); | ||
|
||
if (!accessCode) { | ||
res.status(401).json({ message: "Unauthorized" }); | ||
return; | ||
} | ||
|
||
const secretKey = process.env.CALENDSO_ENCRYPTION_KEY || ""; | ||
|
||
const payloadAuthToken: OAuthTokenPayload = { | ||
userId: accessCode.userId, | ||
teamId: accessCode.teamId, | ||
scope: accessCode.scopes, | ||
token_type: "Access Token", | ||
clientId: client_id, | ||
}; | ||
|
||
const payloadRefreshToken: OAuthTokenPayload = { | ||
userId: accessCode.userId, | ||
teamId: accessCode.teamId, | ||
scope: accessCode.scopes, | ||
token_type: "Refresh Token", | ||
clientId: client_id, | ||
}; | ||
|
||
const access_token = jwt.sign(payloadAuthToken, secretKey, { | ||
expiresIn: 1800, // 30 min | ||
}); | ||
|
||
const refresh_token = jwt.sign(payloadRefreshToken, secretKey, { | ||
expiresIn: 30 * 24 * 60 * 60, // 30 days | ||
}); | ||
|
||
res.status(200).json({ access_token, refresh_token }); | ||
} |
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,4 @@ | ||
import { createNextApiHandler } from "@calcom/trpc/server/createNextApiHandler"; | ||
import { oAuthRouter } from "@calcom/trpc/server/routers/viewer/oAuth/_router"; | ||
|
||
export default createNextApiHandler(oAuthRouter); |
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,179 @@ | ||
import { useSession } from "next-auth/react"; | ||
import { useRouter } from "next/navigation"; | ||
import { useSearchParams } from "next/navigation"; | ||
import { useState, useEffect } from "react"; | ||
|
||
import { APP_NAME } from "@calcom/lib/constants"; | ||
import { useLocale } from "@calcom/lib/hooks/useLocale"; | ||
import { trpc } from "@calcom/trpc/react"; | ||
import { Avatar, Button, Select } from "@calcom/ui"; | ||
import { Plus, Info } from "@calcom/ui/components/icon"; | ||
|
||
import PageWrapper from "@components/PageWrapper"; | ||
|
||
export default function Authorize() { | ||
const { t } = useLocale(); | ||
const { status } = useSession(); | ||
|
||
const router = useRouter(); | ||
const searchParams = useSearchParams(); | ||
|
||
const client_id = searchParams?.get("client_id") as string; | ||
const state = searchParams?.get("state") as string; | ||
const scope = searchParams?.get("scope") as string; | ||
|
||
const queryString = searchParams.toString(); | ||
|
||
const [selectedAccount, setSelectedAccount] = useState<{ value: string; label: string } | null>(); | ||
const scopes = scope ? scope.toString().split(",") : []; | ||
|
||
const { data: client, isLoading: isLoadingGetClient } = trpc.viewer.oAuth.getClient.useQuery( | ||
{ | ||
clientId: client_id as string, | ||
}, | ||
{ | ||
enabled: status !== "loading", | ||
} | ||
); | ||
|
||
const { data, isLoading: isLoadingProfiles } = trpc.viewer.teamsAndUserProfilesQuery.useQuery(); | ||
|
||
const generateAuthCodeMutation = trpc.viewer.oAuth.generateAuthCode.useMutation({ | ||
onSuccess: (data) => { | ||
window.location.href = `${client?.redirectUri}?code=${data.authorizationCode}&state=${state}`; | ||
}, | ||
}); | ||
|
||
const mappedProfiles = data | ||
? data | ||
.filter((profile) => !profile.readOnly) | ||
.map((profile) => ({ | ||
label: profile.name || profile.slug || "", | ||
value: profile.slug || "", | ||
})) | ||
: []; | ||
|
||
useEffect(() => { | ||
if (mappedProfiles.length > 0) { | ||
setSelectedAccount(mappedProfiles[0]); | ||
} | ||
}, [isLoadingProfiles]); | ||
|
||
useEffect(() => { | ||
if (status === "unauthenticated") { | ||
const urlSearchParams = new URLSearchParams({ | ||
callbackUrl: `auth/oauth2/authorize?${queryString}`, | ||
}); | ||
router.replace(`/auth/login?${urlSearchParams.toString()}`); | ||
} | ||
}, [status]); | ||
|
||
const isLoading = isLoadingGetClient || isLoadingProfiles || status !== "authenticated"; | ||
|
||
if (isLoading) { | ||
return <></>; | ||
} | ||
|
||
if (!client) { | ||
return <div>{t("unauthorized")}</div>; | ||
} | ||
|
||
return ( | ||
<div className="flex min-h-screen items-center justify-center"> | ||
<div className="mt-2 max-w-xl rounded-md bg-white px-9 pb-3 pt-2"> | ||
<div className="flex items-center justify-center"> | ||
<Avatar | ||
alt="" | ||
fallback={<Plus className="text-subtle h-6 w-6" />} | ||
className="items-center" | ||
imageSrc={client.logo} | ||
size="lg" | ||
/> | ||
<div className="relative -ml-6 h-24 w-24"> | ||
<div className="absolute inset-0 flex items-center justify-center"> | ||
<div className="flex h-[70px] w-[70px] items-center justify-center rounded-full bg-white"> | ||
<img src="/cal-com-icon.svg" alt="Logo" className="h-16 w-16 rounded-full" /> | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
<h1 className="px-5 pb-5 pt-3 text-center text-2xl font-bold tracking-tight"> | ||
{t("access_cal_account", { clientName: client.name, appName: APP_NAME })} | ||
</h1> | ||
<div className="mb-1 text-sm font-medium">{t("select_account_team")}</div> | ||
<Select | ||
isSearchable={true} | ||
id="account-select" | ||
onChange={(value) => { | ||
setSelectedAccount(value); | ||
}} | ||
className="w-52" | ||
defaultValue={selectedAccount || mappedProfiles[0]} | ||
options={mappedProfiles} | ||
/> | ||
<div className="mb-4 mt-5 font-medium">{t("allow_client_to", { clientName: client.name })}</div> | ||
<ul className="space-y-4 text-sm"> | ||
<li className="relative pl-5"> | ||
<span className="absolute left-0">✓</span>{" "} | ||
{t("associate_with_cal_account", { clientName: client.name })} | ||
</li> | ||
<li className="relative pl-5"> | ||
<span className="absolute left-0">✓</span> {t("see_personal_info")} | ||
</li> | ||
<li className="relative pl-5"> | ||
<span className="absolute left-0">✓</span> {t("see_primary_email_address")} | ||
</li> | ||
<li className="relative pl-5"> | ||
<span className="absolute left-0">✓</span> {t("connect_installed_apps")} | ||
</li> | ||
<li className="relative pl-5"> | ||
<span className="absolute left-0">✓</span> {t("access_event_type")} | ||
</li> | ||
<li className="relative pl-5"> | ||
<span className="absolute left-0">✓</span> {t("access_availability")} | ||
</li> | ||
<li className="relative pl-5"> | ||
<span className="absolute left-0">✓</span> {t("access_bookings")} | ||
</li> | ||
</ul> | ||
<div className="bg-subtle mb-8 mt-8 flex rounded-md p-3"> | ||
<div> | ||
<Info className="mr-1 mt-0.5 h-4 w-4" /> | ||
</div> | ||
<div className="ml-1 "> | ||
<div className="mb-1 text-sm font-medium"> | ||
{t("allow_client_to_do", { clientName: client.name })} | ||
</div> | ||
<div className="text-sm">{t("oauth_access_information", { appName: APP_NAME })}</div>{" "} | ||
</div> | ||
</div> | ||
<div className="border-subtle border- -mx-9 mb-4 border-b" /> | ||
<div className="flex justify-end"> | ||
<Button | ||
className="mr-2" | ||
color="minimal" | ||
onClick={() => { | ||
window.location.href = `${client.redirectUri}`; | ||
}}> | ||
{t("go_back")} | ||
</Button> | ||
<Button | ||
onClick={() => { | ||
generateAuthCodeMutation.mutate({ | ||
clientId: client_id as string, | ||
scopes, | ||
teamSlug: selectedAccount?.value.startsWith("team/") | ||
? selectedAccount?.value.substring(5) | ||
: undefined, // team account starts with /team/<slug> | ||
}); | ||
}} | ||
data-testid="allow-button"> | ||
{t("allow")} | ||
</Button> | ||
</div> | ||
</div> | ||
</div> | ||
); | ||
} | ||
|
||
Authorize.PageWrapper = PageWrapper; |
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,11 @@ | ||
import PageWrapper from "@components/PageWrapper"; | ||
import { getLayout } from "@components/auth/layouts/AdminLayout"; | ||
|
||
import OAuthView from "./oAuthView"; | ||
|
||
const OAuthPage = () => <OAuthView />; | ||
|
||
OAuthPage.getLayout = getLayout; | ||
OAuthPage.PageWrapper = PageWrapper; | ||
|
||
export default OAuthPage; |
Oops, something went wrong.