diff --git a/apps/api/v2/src/ee/calendars/controllers/calendars.controller.e2e-spec.ts b/apps/api/v2/src/ee/calendars/controllers/calendars.controller.e2e-spec.ts index f306e2bd81..03dd039a02 100644 --- a/apps/api/v2/src/ee/calendars/controllers/calendars.controller.e2e-spec.ts +++ b/apps/api/v2/src/ee/calendars/controllers/calendars.controller.e2e-spec.ts @@ -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); diff --git a/apps/api/v2/src/ee/calendars/controllers/calendars.controller.ts b/apps/api/v2/src/ee/calendars/controllers/calendars.controller.ts index 99bb84b92d..57530d686c 100644 --- a/apps/api/v2/src/ee/calendars/controllers/calendars.controller.ts +++ b/apps/api/v2/src/ee/calendars/controllers/calendars.controller.ts @@ -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> { 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: ", diff --git a/apps/api/v2/src/ee/calendars/services/gcal.service.ts b/apps/api/v2/src/ee/calendars/services/gcal.service.ts index 2a683be6bf..9c5b81e766 100644 --- a/apps/api/v2/src/ee/calendars/services/gcal.service.ts +++ b/apps/api/v2/src/ee/calendars/services/gcal.service.ts @@ -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); diff --git a/apps/api/v2/src/ee/calendars/services/outlook.service.ts b/apps/api/v2/src/ee/calendars/services/outlook.service.ts index 3282975042..67c36fe836 100644 --- a/apps/api/v2/src/ee/calendars/services/outlook.service.ts +++ b/apps/api/v2/src/ee/calendars/services/outlook.service.ts @@ -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); diff --git a/packages/platform/atoms/connect/OAuthConnect.tsx b/packages/platform/atoms/connect/OAuthConnect.tsx index 55d398c867..3d4918b309 100644 --- a/packages/platform/atoms/connect/OAuthConnect.tsx +++ b/packages/platform/atoms/connect/OAuthConnect.tsx @@ -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, diff --git a/packages/platform/atoms/connect/apple/AppleConnect.tsx b/packages/platform/atoms/connect/apple/AppleConnect.tsx index 8328427f41..c543d74744 100644 --- a/packages/platform/atoms/connect/apple/AppleConnect.tsx +++ b/packages/platform/atoms/connect/apple/AppleConnect.tsx @@ -38,6 +38,7 @@ export const AppleConnect: FC>> = ({ tooltipSide = "bottom", isClickable, onSuccess, + isDryRun = false, }) => { const { t } = useLocale(); const form = useForm({ @@ -134,7 +135,16 @@ export const AppleConnect: FC>> = ({ 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 }); + } }}>
> = ({ tooltipSide, tooltip, isClickable, + isDryRun, }) => { const { t } = useLocale(); return ( @@ -36,6 +37,7 @@ export const GcalConnect: FC> = ({ tooltipSide={tooltipSide} tooltip={tooltip} isClickable={isClickable} + isDryRun={isDryRun} /> ); }; diff --git a/packages/platform/atoms/connect/outlook/OutlookConnect.tsx b/packages/platform/atoms/connect/outlook/OutlookConnect.tsx index adeee47516..0c75348a2c 100644 --- a/packages/platform/atoms/connect/outlook/OutlookConnect.tsx +++ b/packages/platform/atoms/connect/outlook/OutlookConnect.tsx @@ -20,6 +20,7 @@ export const OutlookConnect: FC> = ({ tooltipSide, tooltip, isClickable, + isDryRun, }) => { const { t } = useLocale(); return ( @@ -36,6 +37,7 @@ export const OutlookConnect: FC> = ({ tooltipSide={tooltipSide} tooltip={tooltip} isClickable={isClickable} + isDryRun={isDryRun} /> ); }; diff --git a/packages/platform/atoms/hooks/connect/useConnect.ts b/packages/platform/atoms/hooks/connect/useConnect.ts index 4b52b82f07..1401f49b6a 100644 --- a/packages/platform/atoms/hooks/connect/useConnect.ts +++ b/packages/platform/atoms/hooks/connect/useConnect.ts @@ -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>( - `/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();