fix: platform gcal connect reconnect credentials (#14513)

* fix: enable gcal connect to reconnect revoked google credentials

* feat: callback to react when check fails
This commit is contained in:
Morgan
2024-04-11 09:36:35 +00:00
committed by GitHub
parent 3fcba33b1b
commit 621067acd8
7 changed files with 94 additions and 11 deletions
+15 -1
View File
@@ -1,3 +1,4 @@
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
import { GcalAuthUrlOutput } from "@/ee/gcal/outputs/auth-url.output";
import { GcalCheckOutput } from "@/ee/gcal/outputs/check.output";
import { GcalSaveRedirectOutput } from "@/ee/gcal/outputs/save-redirect.output";
@@ -41,6 +42,7 @@ const CALENDAR_SCOPES = [
"https://www.googleapis.com/auth/calendar.events",
];
// Controller for the GCalConnect Atom
@Controller({
path: "ee/gcal",
version: "2",
@@ -54,7 +56,8 @@ export class GcalController {
private readonly tokensRepository: TokensRepository,
private readonly selectedCalendarsRepository: SelectedCalendarsRepository,
private readonly config: ConfigService,
private readonly gcalService: GCalService
private readonly gcalService: GCalService,
private readonly calendarsService: CalendarsService
) {}
private redirectUri = `${this.config.get("api.url")}/ee/gcal/oauth/save`;
@@ -148,6 +151,17 @@ export class GcalController {
throw new BadRequestException("Invalid google oauth credentials.");
}
const { connectedCalendars } = await this.calendarsService.getCalendars(userId);
const googleCalendar = connectedCalendars.find(
(cal: { integration: { type: string } }) => cal.integration.type === GOOGLE_CALENDAR_TYPE
);
if (!googleCalendar) {
throw new UnauthorizedException("Google Calendar not connected.");
}
if (googleCalendar.error?.message) {
throw new UnauthorizedException(googleCalendar.error?.message);
}
return { status: SUCCESS_STATUS };
}
}
+11 -1
View File
@@ -1,3 +1,4 @@
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
import { GcalController } from "@/ee/gcal/gcal.controller";
import { AppsRepository } from "@/modules/apps/apps.repository";
import { GCalService } from "@/modules/apps/services/gcal.service";
@@ -6,12 +7,21 @@ import { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { UsersRepository } from "@/modules/users/users.repository";
import { Module } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
@Module({
imports: [PrismaModule, TokensModule, OAuthClientModule],
providers: [AppsRepository, ConfigService, CredentialsRepository, SelectedCalendarsRepository, GCalService],
providers: [
AppsRepository,
ConfigService,
CredentialsRepository,
SelectedCalendarsRepository,
GCalService,
CalendarsService,
UsersRepository,
],
controllers: [GcalController],
})
export class GcalModule {}
@@ -9,16 +9,29 @@ import { APPS_TYPE_ID_MAPPING } from "@calcom/platform-constants";
export class CredentialsRepository {
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
createAppCredential(type: keyof typeof APPS_TYPE_ID_MAPPING, key: Prisma.InputJsonValue, userId: number) {
return this.dbWrite.prisma.credential.create({
data: {
async createAppCredential(
type: keyof typeof APPS_TYPE_ID_MAPPING,
key: Prisma.InputJsonValue,
userId: number
) {
const credential = await this.getByTypeAndUserId(type, userId);
return this.dbWrite.prisma.credential.upsert({
create: {
type,
key,
userId,
appId: APPS_TYPE_ID_MAPPING[type],
},
update: {
key,
invalid: false,
},
where: {
id: credential?.id,
},
});
}
getByTypeAndUserId(type: string, userId: number) {
return this.dbWrite.prisma.credential.findFirst({ where: { type, userId } });
}
@@ -7,13 +7,26 @@ export class SelectedCalendarsRepository {
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
createSelectedCalendar(externalId: string, credentialId: number, userId: number, integration: string) {
return this.dbWrite.prisma.selectedCalendar.create({
data: {
return this.dbWrite.prisma.selectedCalendar.upsert({
create: {
userId,
externalId,
credentialId,
integration,
},
update: {
userId,
externalId,
credentialId,
integration,
},
where: {
userId_integration_externalId: {
userId,
integration,
externalId,
},
},
});
}
@@ -3,6 +3,7 @@ import type { FC } from "react";
import { Button } from "@calcom/ui";
import { useAtomsContext } from "../hooks/useAtomsContext";
import type { OnCheckErroType } from "../hooks/useGcal";
import { useGcal } from "../hooks/useGcal";
import { AtomsWrapper } from "../src/components/atoms-wrapper";
import { cn } from "../src/lib/utils";
@@ -11,16 +12,41 @@ interface GcalConnectProps {
className?: string;
label?: string;
alreadyConnectedLabel?: string;
onCheckError?: OnCheckErroType;
}
/**
* Renders a button to connect or disconnect the Google Calendar of a user.
* @requires AccessToken - The user must be authenticated with an access token passed to CalProvider.
* @component
* @example
* ```tsx
* <GcalConnect
* label="Connect Google Calendar"
* alreadyConnectedLabel="Connected Google Calendar"
* className="my-button"
* />
* ```
*
*
* @param {string} [label="Connect Google Calendar"] - The label for the connect button. Optional.
* @param {string} [alreadyConnectedLabel="Connected Google Calendar"] - The label for the already connected button. Optional.
* @param {string} [className] - Additional CSS class name for the button. Optional.
* @param {OnCheckErroType} [onCheckError] - A callback function to handle errors when checking the connection status. Optional.
* @returns {JSX.Element} The rendered component.
*/
export const GcalConnect: FC<GcalConnectProps> = ({
label = "Connect Google Calendar",
alreadyConnectedLabel = "Connected Google Calendar",
className,
onCheckError,
}) => {
const { isAuth } = useAtomsContext();
const { allowConnect, checked, redirectToGcalOAuth } = useGcal({ isAuth });
const { allowConnect, checked, redirectToGcalOAuth } = useGcal({
isAuth,
onCheckError,
});
if (!isAuth || !checked) return <></>;
+9 -2
View File
@@ -1,12 +1,16 @@
import { useState, useEffect } from "react";
import type { ApiErrorResponse } from "@calcom/platform-types";
import http from "../lib/http";
export type OnCheckErroType = (err: ApiErrorResponse) => void;
export interface useGcalProps {
isAuth: boolean;
onCheckError?: OnCheckErroType;
}
export const useGcal = ({ isAuth }: useGcalProps) => {
export const useGcal = ({ isAuth, onCheckError }: useGcalProps) => {
const [allowConnect, setAllowConnect] = useState<boolean>(false);
const [checked, setChecked] = useState<boolean>(false);
@@ -26,7 +30,10 @@ export const useGcal = ({ isAuth }: useGcalProps) => {
http
?.get("/ee/gcal/check")
.then(() => setAllowConnect(false))
.catch(() => setAllowConnect(true))
.catch((err) => {
setAllowConnect(true);
onCheckError?.(err as ApiErrorResponse);
})
.finally(() => setChecked(true));
}
}, [isAuth]);
@@ -27,7 +27,7 @@ export default function Calendars(props: { calUsername: string; calEmail: string
connectedCalendars.map((connectedCalendar) => (
<div key={connectedCalendar.credentialId}>
<h1 className="text-md font-bold">{connectedCalendar.integration.name}</h1>
{connectedCalendar.calendars.map((calendar) => (
{connectedCalendar.calendars?.map((calendar) => (
<div key={calendar.id}>
<h2>{calendar.name}</h2>
</div>