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:
@@ -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 };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 <></>;
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user