feat: dry run behaviour for connect atoms (#20699)
* add dry run prop for mocking actual behaviour without actual api calls * fix e2e tests * fixup * remove await present twice * fix: adjust success toast for apple connect dry run Co-Authored-By: rajiv@cal.com <rajiv@cal.com> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: rajiv@cal.com <rajiv@cal.com>
This commit is contained in:
co-authored by
rajiv@cal.com <rajiv@cal.com>
Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
rajiv@cal.com <rajiv@cal.com>
parent
b1f50fe22d
commit
d66f983f56
@@ -129,7 +129,7 @@ describe("Platform Calendars Endpoints", () => {
|
||||
|
||||
it(`/GET/v2/calendars/${GOOGLE_CALENDAR}/connect: it should redirect to auth-url for google calendar OAuth with valid access token `, async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/v2/calendars/${GOOGLE_CALENDAR}/connect`)
|
||||
.get(`/v2/calendars/${GOOGLE_CALENDAR}/connect?redir=https://cal.com&isDryRun=false`)
|
||||
.set("Authorization", `Bearer ${accessTokenSecret}`)
|
||||
.set("Origin", CLIENT_REDIRECT_URI)
|
||||
.expect(200);
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
BadRequestException,
|
||||
Post,
|
||||
Body,
|
||||
ParseBoolPipe,
|
||||
} from "@nestjs/common";
|
||||
import { ApiHeader, ApiOperation, ApiParam, ApiQuery, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
import { User } from "@prisma/client";
|
||||
@@ -148,13 +149,14 @@ export class CalendarsController {
|
||||
@Req() req: Request,
|
||||
@Headers("Authorization") authorization: string,
|
||||
@Param("calendar") calendar: string,
|
||||
@Query("redir") redir?: string | null
|
||||
@Query("redir") redir?: string | null,
|
||||
@Query("isDryRun", ParseBoolPipe) isDryRun?: boolean
|
||||
): Promise<ApiResponse<{ authUrl: string }>> {
|
||||
switch (calendar) {
|
||||
case OFFICE_365_CALENDAR:
|
||||
return await this.outlookService.connect(authorization, req, redir ?? "");
|
||||
return await this.outlookService.connect(authorization, req, redir ?? "", isDryRun);
|
||||
case GOOGLE_CALENDAR:
|
||||
return await this.googleCalendarService.connect(authorization, req, redir ?? "");
|
||||
return await this.googleCalendarService.connect(authorization, req, redir ?? "", isDryRun);
|
||||
default:
|
||||
throw new BadRequestException(
|
||||
"Invalid calendar type, available calendars are: ",
|
||||
@@ -179,18 +181,30 @@ export class CalendarsController {
|
||||
): Promise<{ url: string }> {
|
||||
// state params contains our user access token
|
||||
const stateParams = new URLSearchParams(state);
|
||||
const { accessToken, origin, redir } = z
|
||||
.object({ accessToken: z.string(), origin: z.string(), redir: z.string().nullish().optional() })
|
||||
const { accessToken, origin, redir, isDryRun } = z
|
||||
.object({
|
||||
accessToken: z.string(),
|
||||
origin: z.string(),
|
||||
redir: z.string().nullish().optional(),
|
||||
isDryRun: z.string().nullish().optional(),
|
||||
})
|
||||
.parse({
|
||||
accessToken: stateParams.get("accessToken"),
|
||||
origin: stateParams.get("origin"),
|
||||
redir: stateParams.get("redir"),
|
||||
isDryRun: stateParams.get("isDryRun"),
|
||||
});
|
||||
switch (calendar) {
|
||||
case OFFICE_365_CALENDAR:
|
||||
return await this.outlookService.save(code, accessToken, origin, redir ?? "");
|
||||
return await this.outlookService.save(code, accessToken, origin, redir ?? "", isDryRun === "true");
|
||||
case GOOGLE_CALENDAR:
|
||||
return await this.googleCalendarService.save(code, accessToken, origin, redir ?? "");
|
||||
return await this.googleCalendarService.save(
|
||||
code,
|
||||
accessToken,
|
||||
origin,
|
||||
redir ?? "",
|
||||
isDryRun === "true"
|
||||
);
|
||||
default:
|
||||
throw new BadRequestException(
|
||||
"Invalid calendar type, available calendars are: ",
|
||||
|
||||
@@ -39,31 +39,38 @@ export class GoogleCalendarService implements OAuthCalendarApp {
|
||||
async connect(
|
||||
authorization: string,
|
||||
req: Request,
|
||||
redir?: string
|
||||
redir?: string,
|
||||
isDryRun?: boolean
|
||||
): Promise<{ status: typeof SUCCESS_STATUS; data: { authUrl: string } }> {
|
||||
const accessToken = authorization.replace("Bearer ", "");
|
||||
const origin = req.get("origin") ?? req.get("host");
|
||||
const redirectUrl = await this.getCalendarRedirectUrl(accessToken, origin ?? "", redir);
|
||||
const redirectUrl = await this.getCalendarRedirectUrl(accessToken, origin ?? "", redir, isDryRun);
|
||||
|
||||
return { status: SUCCESS_STATUS, data: { authUrl: redirectUrl } };
|
||||
}
|
||||
|
||||
async save(code: string, accessToken: string, origin: string, redir?: string): Promise<{ url: string }> {
|
||||
return await this.saveCalendarCredentialsAndRedirect(code, accessToken, origin, redir);
|
||||
async save(
|
||||
code: string,
|
||||
accessToken: string,
|
||||
origin: string,
|
||||
redir?: string,
|
||||
isDryRun?: boolean
|
||||
): Promise<{ url: string }> {
|
||||
return await this.saveCalendarCredentialsAndRedirect(code, accessToken, origin, redir, isDryRun);
|
||||
}
|
||||
|
||||
async check(userId: number): Promise<{ status: typeof SUCCESS_STATUS }> {
|
||||
return await this.checkIfCalendarConnected(userId);
|
||||
}
|
||||
|
||||
async getCalendarRedirectUrl(accessToken: string, origin: string, redir?: string) {
|
||||
async getCalendarRedirectUrl(accessToken: string, origin: string, redir?: string, isDryRun?: boolean) {
|
||||
const oAuth2Client = await this.getOAuthClient(this.redirectUri);
|
||||
|
||||
const authUrl = oAuth2Client.generateAuthUrl({
|
||||
access_type: "offline",
|
||||
scope: CALENDAR_SCOPES,
|
||||
prompt: "consent",
|
||||
state: `accessToken=${accessToken}&origin=${origin}&redir=${redir ?? ""}`,
|
||||
state: `accessToken=${accessToken}&origin=${origin}&redir=${redir ?? ""}&isDryRun=${isDryRun}`,
|
||||
});
|
||||
|
||||
return authUrl;
|
||||
@@ -115,7 +122,8 @@ export class GoogleCalendarService implements OAuthCalendarApp {
|
||||
code: string,
|
||||
accessToken: string,
|
||||
origin: string,
|
||||
redir?: string
|
||||
redir?: string,
|
||||
isDryRun?: boolean
|
||||
) {
|
||||
// User chose not to authorize your app or didn't authorize your app
|
||||
// redirect directly without oauth code
|
||||
@@ -123,6 +131,11 @@ export class GoogleCalendarService implements OAuthCalendarApp {
|
||||
return { url: redir || origin };
|
||||
}
|
||||
|
||||
// if isDryRun is true we know its a dry run so we just redirect straight away
|
||||
if (isDryRun) {
|
||||
return { url: redir || origin };
|
||||
}
|
||||
|
||||
const parsedCode = z.string().parse(code);
|
||||
|
||||
const ownerId = await this.tokensRepository.getAccessTokenOwnerId(accessToken);
|
||||
|
||||
@@ -33,24 +33,31 @@ export class OutlookService implements OAuthCalendarApp {
|
||||
async connect(
|
||||
authorization: string,
|
||||
req: Request,
|
||||
redir?: string
|
||||
redir?: string,
|
||||
isDryRun?: boolean
|
||||
): Promise<{ status: typeof SUCCESS_STATUS; data: { authUrl: string } }> {
|
||||
const accessToken = authorization.replace("Bearer ", "");
|
||||
const origin = req.get("origin") ?? req.get("host");
|
||||
const redirectUrl = await await this.getCalendarRedirectUrl(accessToken, origin ?? "", redir);
|
||||
const redirectUrl = await this.getCalendarRedirectUrl(accessToken, origin ?? "", redir, isDryRun);
|
||||
|
||||
return { status: SUCCESS_STATUS, data: { authUrl: redirectUrl } };
|
||||
}
|
||||
|
||||
async save(code: string, accessToken: string, origin: string, redir?: string): Promise<{ url: string }> {
|
||||
return await this.saveCalendarCredentialsAndRedirect(code, accessToken, origin, redir);
|
||||
async save(
|
||||
code: string,
|
||||
accessToken: string,
|
||||
origin: string,
|
||||
redir?: string,
|
||||
isDryRun?: boolean
|
||||
): Promise<{ url: string }> {
|
||||
return await this.saveCalendarCredentialsAndRedirect(code, accessToken, origin, redir, isDryRun);
|
||||
}
|
||||
|
||||
async check(userId: number): Promise<{ status: typeof SUCCESS_STATUS }> {
|
||||
return await this.checkIfCalendarConnected(userId);
|
||||
}
|
||||
|
||||
async getCalendarRedirectUrl(accessToken: string, origin: string, redir?: string) {
|
||||
async getCalendarRedirectUrl(accessToken: string, origin: string, redir?: string, isDryRun?: boolean) {
|
||||
const { client_id } = await this.calendarsService.getAppKeys(OFFICE_365_CALENDAR_ID);
|
||||
|
||||
const scopes = ["User.Read", "Calendars.Read", "Calendars.ReadWrite", "offline_access"];
|
||||
@@ -60,7 +67,7 @@ export class OutlookService implements OAuthCalendarApp {
|
||||
client_id,
|
||||
prompt: "select_account",
|
||||
redirect_uri: this.redirectUri,
|
||||
state: `accessToken=${accessToken}&origin=${origin}&redir=${redir ?? ""}`,
|
||||
state: `accessToken=${accessToken}&origin=${origin}&redir=${redir ?? ""}&isDryRun=${isDryRun}`,
|
||||
};
|
||||
|
||||
const query = stringify(params);
|
||||
@@ -148,13 +155,19 @@ export class OutlookService implements OAuthCalendarApp {
|
||||
code: string,
|
||||
accessToken: string,
|
||||
origin: string,
|
||||
redir?: string
|
||||
redir?: string,
|
||||
isDryRun?: boolean
|
||||
) {
|
||||
// if code is not defined, user denied to authorize office 365 app, just redirect straight away
|
||||
if (!code || code === "undefined") {
|
||||
return { url: redir || origin };
|
||||
}
|
||||
|
||||
// if isDryRun is true we know its a dry run so we just redirect straight away
|
||||
if (isDryRun) {
|
||||
return { url: redir || origin };
|
||||
}
|
||||
|
||||
const parsedCode = z.string().parse(code);
|
||||
|
||||
const ownerId = await this.tokensRepository.getAccessTokenOwnerId(accessToken);
|
||||
|
||||
@@ -26,6 +26,7 @@ export type OAuthConnectProps = {
|
||||
tooltipSide?: "top" | "bottom" | "left" | "right";
|
||||
isClickable?: boolean;
|
||||
onSuccess?: () => void;
|
||||
isDryRun?: boolean;
|
||||
};
|
||||
|
||||
export const OAuthConnect: FC<
|
||||
@@ -46,8 +47,9 @@ export const OAuthConnect: FC<
|
||||
tooltipSide = "bottom",
|
||||
isClickable,
|
||||
onSuccess,
|
||||
isDryRun,
|
||||
}) => {
|
||||
const { connect } = useConnect(calendar, redir);
|
||||
const { connect } = useConnect(calendar, redir, isDryRun);
|
||||
const { allowConnect, checked } = useCheck({
|
||||
onCheckError,
|
||||
calendar: calendar,
|
||||
|
||||
@@ -38,6 +38,7 @@ export const AppleConnect: FC<Partial<Omit<OAuthConnectProps, "redir">>> = ({
|
||||
tooltipSide = "bottom",
|
||||
isClickable,
|
||||
onSuccess,
|
||||
isDryRun = false,
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
const form = useForm({
|
||||
@@ -134,7 +135,16 @@ export const AppleConnect: FC<Partial<Omit<OAuthConnectProps, "redir">>> = ({
|
||||
handleSubmit={async (values) => {
|
||||
const { username, password } = values;
|
||||
|
||||
await saveCredentials({ calendar: "apple", username, password });
|
||||
if (isDryRun) {
|
||||
form.reset();
|
||||
setIsDialogOpen(false);
|
||||
toast({
|
||||
description: "Calendar credentials added successfully",
|
||||
});
|
||||
onSuccess?.();
|
||||
} else {
|
||||
await saveCredentials({ calendar: "apple", username, password });
|
||||
}
|
||||
}}>
|
||||
<fieldset
|
||||
className="space-y-4"
|
||||
|
||||
@@ -20,6 +20,7 @@ export const GcalConnect: FC<Partial<OAuthConnectProps>> = ({
|
||||
tooltipSide,
|
||||
tooltip,
|
||||
isClickable,
|
||||
isDryRun,
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
return (
|
||||
@@ -36,6 +37,7 @@ export const GcalConnect: FC<Partial<OAuthConnectProps>> = ({
|
||||
tooltipSide={tooltipSide}
|
||||
tooltip={tooltip}
|
||||
isClickable={isClickable}
|
||||
isDryRun={isDryRun}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -20,6 +20,7 @@ export const OutlookConnect: FC<Partial<OAuthConnectProps>> = ({
|
||||
tooltipSide,
|
||||
tooltip,
|
||||
isClickable,
|
||||
isDryRun,
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
return (
|
||||
@@ -36,6 +37,7 @@ export const OutlookConnect: FC<Partial<OAuthConnectProps>> = ({
|
||||
tooltipSide={tooltipSide}
|
||||
tooltip={tooltip}
|
||||
isClickable={isClickable}
|
||||
isDryRun={isDryRun}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -13,7 +13,11 @@ interface IPUpdateOAuthCredentials {
|
||||
onError?: (err: ApiErrorResponse) => void;
|
||||
}
|
||||
|
||||
export const useGetRedirectUrl = (calendar: (typeof CALENDARS)[number], redir?: string) => {
|
||||
export const useGetRedirectUrl = (
|
||||
calendar: (typeof CALENDARS)[number],
|
||||
redir?: string,
|
||||
isDryRun?: boolean
|
||||
) => {
|
||||
const authUrl = useQuery({
|
||||
queryKey: getQueryKey(calendar),
|
||||
staleTime: Infinity,
|
||||
@@ -21,7 +25,7 @@ export const useGetRedirectUrl = (calendar: (typeof CALENDARS)[number], redir?:
|
||||
queryFn: () => {
|
||||
return http
|
||||
?.get<ApiResponse<{ authUrl: string }>>(
|
||||
`/calendars/${calendar}/connect${redir ? `?redir=${redir}` : ""}`
|
||||
`/calendars/${calendar}/connect?redir=${redir ?? ""}&isDryRun=${isDryRun ?? ""}`
|
||||
)
|
||||
.then(({ data: responseBody }) => {
|
||||
if (responseBody.status === SUCCESS_STATUS) {
|
||||
@@ -36,8 +40,8 @@ export const useGetRedirectUrl = (calendar: (typeof CALENDARS)[number], redir?:
|
||||
return authUrl;
|
||||
};
|
||||
|
||||
export const useConnect = (calendar: (typeof CALENDARS)[number], redir?: string) => {
|
||||
const { refetch } = useGetRedirectUrl(calendar, redir);
|
||||
export const useConnect = (calendar: (typeof CALENDARS)[number], redir?: string, isDryRun?: boolean) => {
|
||||
const { refetch } = useGetRedirectUrl(calendar, redir, isDryRun);
|
||||
|
||||
const connect = async () => {
|
||||
const redirectUri = await refetch();
|
||||
|
||||
Reference in New Issue
Block a user