feat: outlook 365 connect atom (#15318)

* custom for office 365 connect atom

* office 365 connect atom

* add office 365 atom to package exports

* fixup

* fixup

* fixup fixup

* refactors

* chore: refactor connect atoms

* fixup

* chore: refactor connect, enable custom redir

* fixup! Merge branch 'outlook-365-calendar-frontend' of github.com:calcom/cal.com into outlook-365-calendar-frontend

* fixup! fixup! Merge branch 'outlook-365-calendar-frontend' of github.com:calcom/cal.com into outlook-365-calendar-frontend

---------

Co-authored-by: Rajiv Sahal <rajivsahal@Rajivs-MacBook-Pro.local>
Co-authored-by: Morgan Vernay <morgan@cal.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
This commit is contained in:
Rajiv Sahal
2024-06-06 15:08:38 +00:00
committed by GitHub
co-authored by Rajiv Sahal Morgan Vernay Morgan
parent 50eeb712e3
commit 7d4e8e2210
17 changed files with 285 additions and 195 deletions
@@ -71,6 +71,7 @@ export class CalendarsController {
}
@Get("/")
@UseGuards(AccessTokenGuard)
async getCalendars(@GetUser("id") userId: number): Promise<ConnectedCalendarsOutput> {
const calendars = await this.calendarsService.getCalendars(userId);
@@ -86,13 +87,14 @@ export class CalendarsController {
async redirect(
@Req() req: Request,
@Headers("Authorization") authorization: string,
@Param("calendar") calendar: string
@Param("calendar") calendar: string,
@Query("redir") redir?: string | null
): Promise<ApiResponse<{ authUrl: string }>> {
switch (calendar) {
case OFFICE_365_CALENDAR:
return await this.outlookService.connect(authorization, req);
return await this.outlookService.connect(authorization, req, redir ?? "");
case GOOGLE_CALENDAR:
return await this.googleCalendarService.connect(authorization, req);
return await this.googleCalendarService.connect(authorization, req, redir ?? "");
default:
throw new BadRequestException(
"Invalid calendar type, available calendars are: ",
@@ -111,15 +113,18 @@ export class CalendarsController {
): Promise<{ url: string }> {
// state params contains our user access token
const stateParams = new URLSearchParams(state);
const { accessToken, origin } = z
.object({ accessToken: z.string(), origin: z.string() })
.parse({ accessToken: stateParams.get("accessToken"), origin: stateParams.get("origin") });
const { accessToken, origin, redir } = z
.object({ accessToken: z.string(), origin: z.string(), redir: z.string().nullish().optional() })
.parse({
accessToken: stateParams.get("accessToken"),
origin: stateParams.get("origin"),
redir: stateParams.get("redir"),
});
switch (calendar) {
case OFFICE_365_CALENDAR:
return await this.outlookService.save(code, accessToken, origin);
return await this.outlookService.save(code, accessToken, origin, redir ?? "");
case GOOGLE_CALENDAR:
return await this.googleCalendarService.save(code, accessToken, origin);
return await this.googleCalendarService.save(code, accessToken, origin, redir ?? "");
default:
throw new BadRequestException(
"Invalid calendar type, available calendars are: ",
@@ -37,31 +37,32 @@ export class GoogleCalendarService implements OAuthCalendarApp {
async connect(
authorization: string,
req: Request
req: Request,
redir?: string
): 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 ?? "");
const redirectUrl = await await this.getCalendarRedirectUrl(accessToken, origin ?? "", redir);
return { status: SUCCESS_STATUS, data: { authUrl: redirectUrl } };
}
async save(code: string, accessToken: string, origin: string): Promise<{ url: string }> {
return await this.saveCalendarCredentialsAndRedirect(code, accessToken, origin);
async save(code: string, accessToken: string, origin: string, redir?: string): Promise<{ url: string }> {
return await this.saveCalendarCredentialsAndRedirect(code, accessToken, origin, redir);
}
async check(userId: number): Promise<{ status: typeof SUCCESS_STATUS }> {
return await this.checkIfCalendarConnected(userId);
}
async getCalendarRedirectUrl(accessToken: string, origin: string) {
async getCalendarRedirectUrl(accessToken: string, origin: string, redir?: string) {
const oAuth2Client = await this.getOAuthClient(this.redirectUri);
const authUrl = oAuth2Client.generateAuthUrl({
access_type: "offline",
scope: CALENDAR_SCOPES,
prompt: "consent",
state: `accessToken=${accessToken}&origin=${origin}`,
state: `accessToken=${accessToken}&origin=${origin}&redir=${redir ?? ""}`,
});
return authUrl;
@@ -106,11 +107,16 @@ export class GoogleCalendarService implements OAuthCalendarApp {
return { status: SUCCESS_STATUS };
}
async saveCalendarCredentialsAndRedirect(code: string, accessToken: string, origin: string) {
async saveCalendarCredentialsAndRedirect(
code: string,
accessToken: string,
origin: string,
redir?: string
) {
// User chose not to authorize your app or didn't authorize your app
// redirect directly without oauth code
if (!code) {
return { url: origin };
return { url: redir || origin };
}
const parsedCode = z.string().parse(code);
@@ -151,6 +157,6 @@ export class GoogleCalendarService implements OAuthCalendarApp {
);
}
return { url: origin };
return { url: redir || origin };
}
}
@@ -11,13 +11,16 @@ import { Request } from "express";
import { stringify } from "querystring";
import { z } from "zod";
import { OFFICE_365_CALENDAR_TYPE } from "@calcom/platform-constants";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { OFFICE_365_CALENDAR_ID } from "@calcom/platform-constants";
import {
SUCCESS_STATUS,
OFFICE_365_CALENDAR,
OFFICE_365_CALENDAR_ID,
OFFICE_365_CALENDAR_TYPE,
} from "@calcom/platform-constants";
@Injectable()
export class OutlookService implements OAuthCalendarApp {
private redirectUri = `${this.config.get("api.url")}/calendars/office365/save`;
private redirectUri = `${this.config.get("api.url")}/calendars/${OFFICE_365_CALENDAR}/save`;
constructor(
private readonly config: ConfigService,
@@ -29,24 +32,25 @@ export class OutlookService implements OAuthCalendarApp {
async connect(
authorization: string,
req: Request
req: Request,
redir?: string
): 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 ?? "");
const redirectUrl = await await this.getCalendarRedirectUrl(accessToken, origin ?? "", redir);
return { status: SUCCESS_STATUS, data: { authUrl: redirectUrl } };
}
async save(code: string, accessToken: string, origin: string): Promise<{ url: string }> {
return await this.saveCalendarCredentialsAndRedirect(code, accessToken, origin);
async save(code: string, accessToken: string, origin: string, redir?: string): Promise<{ url: string }> {
return await this.saveCalendarCredentialsAndRedirect(code, accessToken, origin, redir);
}
async check(userId: number): Promise<{ status: typeof SUCCESS_STATUS }> {
return await this.checkIfCalendarConnected(userId);
}
async getCalendarRedirectUrl(accessToken: string, origin: string) {
async getCalendarRedirectUrl(accessToken: string, origin: string, redir?: string) {
const { client_id } = await this.calendarsService.getAppKeys(OFFICE_365_CALENDAR_ID);
const scopes = ["User.Read", "Calendars.Read", "Calendars.ReadWrite", "offline_access"];
@@ -56,7 +60,7 @@ export class OutlookService implements OAuthCalendarApp {
client_id,
prompt: "select_account",
redirect_uri: this.redirectUri,
state: `accessToken=${accessToken}&origin=${origin}`,
state: `accessToken=${accessToken}&origin=${origin}&redir=${redir ?? ""}`,
};
const query = stringify(params);
@@ -140,10 +144,15 @@ export class OutlookService implements OAuthCalendarApp {
return responseBody as OfficeCalendar;
}
async saveCalendarCredentialsAndRedirect(code: string, accessToken: string, origin: string) {
async saveCalendarCredentialsAndRedirect(
code: string,
accessToken: string,
origin: string,
redir?: string
) {
// if code is not defined, user denied to authorize office 365 app, just redirect straight away
if (!code) {
return { url: origin };
return { url: redir || origin };
}
const parsedCode = z.string().parse(code);
@@ -174,7 +183,7 @@ export class OutlookService implements OAuthCalendarApp {
}
return {
url: origin,
url: redir || origin,
};
}
}
+4 -50
View File
@@ -82,56 +82,10 @@ export class GcalController {
@Redirect(undefined, 301)
@HttpCode(HttpStatus.OK)
async save(@Query("state") state: string, @Query("code") code: string): Promise<GcalSaveRedirectOutput> {
const stateParams = new URLSearchParams(state);
const { accessToken, origin } = z
.object({ accessToken: z.string(), origin: z.string() })
.parse({ accessToken: stateParams.get("accessToken"), origin: stateParams.get("origin") });
// User chose not to authorize your app or didn't authorize your app
// redirect directly without oauth code
if (!code) {
return { url: origin };
}
const parsedCode = z.string().parse(code);
const ownerId = await this.tokensRepository.getAccessTokenOwnerId(accessToken);
if (!ownerId) {
throw new UnauthorizedException("Invalid Access token.");
}
const oAuth2Client = await this.gcalService.getOAuthClient(this.redirectUri);
const token = await oAuth2Client.getToken(parsedCode);
// Google oAuth Credentials are stored in token.tokens
const key = token.tokens;
const credential = await this.credentialRepository.createAppCredential(
GOOGLE_CALENDAR_TYPE,
key as Prisma.InputJsonValue,
ownerId
);
oAuth2Client.setCredentials(key);
const calendar = google.calendar({
version: "v3",
auth: oAuth2Client,
});
const cals = await calendar.calendarList.list({ fields: "items(id,summary,primary,accessRole)" });
const primaryCal = cals.data.items?.find((cal) => cal.primary);
if (primaryCal?.id) {
await this.selectedCalendarsRepository.createSelectedCalendar(
primaryCal.id,
credential.id,
ownerId,
GOOGLE_CALENDAR_TYPE
);
}
return { url: origin };
const url = new URL(this.config.get("api.url") + "/calendars/google/save");
url.searchParams.append("code", code);
url.searchParams.append("state", state);
return { url: url.href };
}
@Get("/check")