Gmail Connector (#946)

---------

Co-authored-by: Yuhong Sun <yuhongsun96@gmail.com>
This commit is contained in:
Itay
2024-01-23 02:25:10 +02:00
committed by GitHub
parent 2c38033ef5
commit 692fdb4597
24 changed files with 1773 additions and 5 deletions

BIN
web/public/Gmail.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

View File

@@ -0,0 +1,437 @@
import { Button } from "@/components/Button";
import { PopupSpec } from "@/components/admin/connectors/Popup";
import { useState } from "react";
import { useSWRConfig } from "swr";
import * as Yup from "yup";
import { useRouter } from "next/navigation";
import {
Credential,
GmailCredentialJson,
GmailServiceAccountCredentialJson,
} from "@/lib/types";
import { adminDeleteCredential } from "@/lib/credential";
import { setupGmailOAuth } from "@/lib/gmail";
import { GMAIL_AUTH_IS_ADMIN_COOKIE_NAME } from "@/lib/constants";
import Cookies from "js-cookie";
import { TextFormField } from "@/components/admin/connectors/Field";
import { Form, Formik } from "formik";
import { Card } from "@tremor/react";
type GmailCredentialJsonTypes = "authorized_user" | "service_account";
const DriveJsonUpload = ({
setPopup,
}: {
setPopup: (popupSpec: PopupSpec | null) => void;
}) => {
const { mutate } = useSWRConfig();
const [credentialJsonStr, setCredentialJsonStr] = useState<
string | undefined
>();
return (
<>
<input
className={
"mr-3 text-sm text-gray-900 border border-gray-300 rounded-lg " +
"cursor-pointer bg-gray-50 dark:text-gray-400 focus:outline-none " +
"dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400"
}
type="file"
accept=".json"
onChange={(event) => {
if (!event.target.files) {
return;
}
const file = event.target.files[0];
const reader = new FileReader();
reader.onload = function (loadEvent) {
if (!loadEvent?.target?.result) {
return;
}
const fileContents = loadEvent.target.result;
setCredentialJsonStr(fileContents as string);
};
reader.readAsText(file);
}}
/>
<Button
disabled={!credentialJsonStr}
onClick={async () => {
// check if the JSON is a app credential or a service account credential
let credentialFileType: GmailCredentialJsonTypes;
try {
const appCredentialJson = JSON.parse(credentialJsonStr!);
if (appCredentialJson.web) {
credentialFileType = "authorized_user";
} else if (appCredentialJson.type === "service_account") {
credentialFileType = "service_account";
} else {
throw new Error(
"Unknown credential type, expected 'OAuth Web application'"
);
}
} catch (e) {
setPopup({
message: `Invalid file provided - ${e}`,
type: "error",
});
return;
}
if (credentialFileType === "authorized_user") {
const response = await fetch(
"/api/manage/admin/connector/gmail/app-credential",
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: credentialJsonStr,
}
);
if (response.ok) {
setPopup({
message: "Successfully uploaded app credentials",
type: "success",
});
} else {
const errorMsg = await response.text();
setPopup({
message: `Failed to upload app credentials - ${errorMsg}`,
type: "error",
});
}
mutate("/api/manage/admin/connector/gmail/app-credential");
}
if (credentialFileType === "service_account") {
const response = await fetch(
"/api/manage/admin/connector/gmail/service-account-key",
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: credentialJsonStr,
}
);
if (response.ok) {
setPopup({
message: "Successfully uploaded app credentials",
type: "success",
});
} else {
const errorMsg = await response.text();
setPopup({
message: `Failed to upload app credentials - ${errorMsg}`,
type: "error",
});
}
mutate("/api/manage/admin/connector/gmail/service-account-key");
}
}}
>
Upload
</Button>
</>
);
};
interface DriveJsonUploadSectionProps {
setPopup: (popupSpec: PopupSpec | null) => void;
appCredentialData?: { client_id: string };
serviceAccountCredentialData?: { service_account_email: string };
}
export const GmailJsonUploadSection = ({
setPopup,
appCredentialData,
serviceAccountCredentialData,
}: DriveJsonUploadSectionProps) => {
const { mutate } = useSWRConfig();
if (serviceAccountCredentialData?.service_account_email) {
return (
<div className="mt-2 text-sm">
<div>
Found existing service account key with the following <b>Email:</b>
<p className="italic mt-1">
{serviceAccountCredentialData.service_account_email}
</p>
</div>
<div className="mt-4 mb-1">
If you want to update these credentials, delete the existing
credentials through the button below, and then upload a new
credentials JSON.
</div>
<Button
onClick={async () => {
const response = await fetch(
"/api/manage/admin/connector/gmail/service-account-key",
{
method: "DELETE",
}
);
if (response.ok) {
mutate("/api/manage/admin/connector/gmail/service-account-key");
setPopup({
message: "Successfully deleted service account key",
type: "success",
});
} else {
const errorMsg = await response.text();
setPopup({
message: `Failed to delete service account key - ${errorMsg}`,
type: "error",
});
}
}}
>
Delete
</Button>
</div>
);
}
if (appCredentialData?.client_id) {
return (
<div className="mt-2 text-sm">
<div>
Found existing app credentials with the following <b>Client ID:</b>
<p className="italic mt-1">{appCredentialData.client_id}</p>
</div>
<div className="mt-4 mb-1">
If you want to update these credentials, delete the existing
credentials through the button below, and then upload a new
credentials JSON.
</div>
<Button
onClick={async () => {
const response = await fetch(
"/api/manage/admin/connector/gmail/app-credential",
{
method: "DELETE",
}
);
if (response.ok) {
mutate("/api/manage/admin/connector/gmail/app-credential");
setPopup({
message: "Successfully deleted service account key",
type: "success",
});
} else {
const errorMsg = await response.text();
setPopup({
message: `Failed to delete app credential - ${errorMsg}`,
type: "error",
});
}
}}
>
Delete
</Button>
</div>
);
}
return (
<div className="mt-2">
<p className="text-sm mb-2">
Follow the guide{" "}
<a
className="text-link"
target="_blank"
href="https://docs.danswer.dev/connectors/gmail#authorization"
>
here
</a>{" "}
to setup a google OAuth App in your company workspace.
<br />
<br />
Download the credentials JSON and upload it here.
</p>
<DriveJsonUpload setPopup={setPopup} />
</div>
);
};
interface DriveCredentialSectionProps {
gmailPublicCredential?: Credential<GmailCredentialJson>;
gmailServiceAccountCredential?: Credential<GmailServiceAccountCredentialJson>;
serviceAccountKeyData?: { service_account_email: string };
appCredentialData?: { client_id: string };
setPopup: (popupSpec: PopupSpec | null) => void;
refreshCredentials: () => void;
connectorExists: boolean;
}
export const GmailOAuthSection = ({
gmailPublicCredential,
gmailServiceAccountCredential,
serviceAccountKeyData,
appCredentialData,
setPopup,
refreshCredentials,
connectorExists,
}: DriveCredentialSectionProps) => {
const router = useRouter();
const existingCredential =
gmailPublicCredential || gmailServiceAccountCredential;
if (existingCredential) {
return (
<>
<p className="mb-2 text-sm">
<i>Existing credential already setup!</i>
</p>
<Button
onClick={async () => {
if (connectorExists) {
setPopup({
message:
"Cannot revoke access to Gmail while any connector is still setup. Please delete all connectors, then try again.",
type: "error",
});
return;
}
await adminDeleteCredential(existingCredential.id);
setPopup({
message: "Successfully revoked access to Gmail!",
type: "success",
});
refreshCredentials();
}}
>
Revoke Access
</Button>
</>
);
}
if (serviceAccountKeyData?.service_account_email) {
return (
<div>
<p className="text-sm mb-2">
When using a Gmail Service Account, you can either have Danswer act as
the service account itself OR you can specify an account for the
service account to impersonate.
<br />
<br />
If you want to use the service account itself, leave the{" "}
<b>&apos;User email to impersonate&apos;</b> field blank when
submitting. If you do choose this option, make sure you have shared
the documents you want to index with the service account.
</p>
<Card>
<Formik
initialValues={{
gmail_delegated_user: "",
}}
validationSchema={Yup.object().shape({
gmail_delegated_user: Yup.string().optional(),
})}
onSubmit={async (values, formikHelpers) => {
formikHelpers.setSubmitting(true);
const response = await fetch(
"/api/manage/admin/connector/gmail/service-account-credential",
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
gmail_delegated_user: values.gmail_delegated_user,
}),
}
);
if (response.ok) {
setPopup({
message: "Successfully created service account credential",
type: "success",
});
} else {
const errorMsg = await response.text();
setPopup({
message: `Failed to create service account credential - ${errorMsg}`,
type: "error",
});
}
refreshCredentials();
}}
>
{({ isSubmitting }) => (
<Form>
<TextFormField
name="gmail_delegated_user"
label="[Optional] User email to impersonate:"
subtext="If left blank, Danswer will use the service account itself."
/>
<div className="flex">
<button
type="submit"
disabled={isSubmitting}
className={
"bg-slate-500 hover:bg-slate-700 text-white " +
"font-bold py-2 px-4 rounded focus:outline-none " +
"focus:shadow-outline w-full max-w-sm mx-auto"
}
>
Submit
</button>
</div>
</Form>
)}
</Formik>
</Card>
</div>
);
}
if (appCredentialData?.client_id) {
return (
<div className="text-sm mb-4">
<p className="mb-2">
Next, you must provide credentials via OAuth. This gives us read
access to the docs you have access to in your gmail account.
</p>
<Button
onClick={async () => {
const [authUrl, errorMsg] = await setupGmailOAuth({
isAdmin: true,
});
if (authUrl) {
// cookie used by callback to determine where to finally redirect to
Cookies.set(GMAIL_AUTH_IS_ADMIN_COOKIE_NAME, "true", {
path: "/",
});
router.push(authUrl);
return;
}
setPopup({
message: errorMsg,
type: "error",
});
}}
>
Authenticate with Gmail
</Button>
</div>
);
}
// case where no keys have been uploaded in step 1
return (
<p className="text-sm">
Please upload a OAuth Client Credential JSON in Step 1 before moving onto
Step 2.
</p>
);
};

View File

@@ -0,0 +1,127 @@
import { BasicTable } from "@/components/admin/connectors/BasicTable";
import { PopupSpec } from "@/components/admin/connectors/Popup";
import { StatusRow } from "@/components/admin/connectors/table/ConnectorsTable";
import { deleteConnector } from "@/lib/connector";
import {
GmailConfig,
ConnectorIndexingStatus,
GmailCredentialJson,
} from "@/lib/types";
import { useSWRConfig } from "swr";
import { DeleteColumn } from "@/components/admin/connectors/table/DeleteColumn";
import {
Table,
TableHead,
TableRow,
TableHeaderCell,
TableBody,
TableCell,
} from "@tremor/react";
interface TableProps {
gmailConnectorIndexingStatuses: ConnectorIndexingStatus<
GmailConfig,
GmailCredentialJson
>[];
setPopup: (popupSpec: PopupSpec | null) => void;
}
export const GmailConnectorsTable = ({
gmailConnectorIndexingStatuses: gmailConnectorIndexingStatuses,
setPopup,
}: TableProps) => {
const { mutate } = useSWRConfig();
// Sorting to maintain a consistent ordering
const sortedGmailConnectorIndexingStatuses = [
...gmailConnectorIndexingStatuses,
];
sortedGmailConnectorIndexingStatuses.sort(
(a, b) => a.connector.id - b.connector.id
);
return (
<div>
<Table className="overflow-visible">
<TableHead>
<TableRow>
<TableHeaderCell>Status</TableHeaderCell>
<TableHeaderCell>Delete</TableHeaderCell>
</TableRow>
</TableHead>
<TableBody>
{sortedGmailConnectorIndexingStatuses.map(
(connectorIndexingStatus) => {
return (
<TableRow key={connectorIndexingStatus.cc_pair_id}>
<TableCell>
<StatusRow
connectorIndexingStatus={connectorIndexingStatus}
hasCredentialsIssue={
connectorIndexingStatus.connector.credential_ids
.length === 0
}
setPopup={setPopup}
onUpdate={() => {
mutate("/api/manage/admin/connector/indexing-status");
}}
/>
</TableCell>
<TableCell>
<DeleteColumn
connectorIndexingStatus={connectorIndexingStatus}
setPopup={setPopup}
onUpdate={() =>
mutate("/api/manage/admin/connector/indexing-status")
}
/>
</TableCell>
</TableRow>
);
}
)}
</TableBody>
</Table>
</div>
);
return (
<BasicTable
columns={[
{
header: "Status",
key: "status",
},
{
header: "Delete",
key: "delete",
},
]}
data={sortedGmailConnectorIndexingStatuses.map(
(connectorIndexingStatus) => ({
status: (
<StatusRow
connectorIndexingStatus={connectorIndexingStatus}
hasCredentialsIssue={
connectorIndexingStatus.connector.credential_ids.length === 0
}
setPopup={setPopup}
onUpdate={() => {
mutate("/api/manage/admin/connector/indexing-status");
}}
/>
),
delete: (
<DeleteColumn
connectorIndexingStatus={connectorIndexingStatus}
setPopup={setPopup}
onUpdate={() =>
mutate("/api/manage/admin/connector/indexing-status")
}
/>
),
})
)}
/>
);
};

View File

@@ -0,0 +1,34 @@
import { getDomain } from "@/lib/redirectSS";
import { buildUrl } from "@/lib/utilsSS";
import { NextRequest, NextResponse } from "next/server";
import { cookies } from "next/headers";
import { GMAIL_AUTH_IS_ADMIN_COOKIE_NAME } from "@/lib/constants";
import { processCookies } from "@/lib/userSS";
export const GET = async (request: NextRequest) => {
// Wrapper around the FastAPI endpoint /connectors/gmail/callback,
// which adds back a redirect to the Gmail admin page.
const url = new URL(buildUrl("/manage/connector/gmail/callback"));
url.search = request.nextUrl.search;
const response = await fetch(url.toString(), {
headers: {
cookie: processCookies(cookies()),
},
});
if (!response.ok) {
console.log("Error in Gmail callback:", (await response.json()).detail);
return NextResponse.redirect(new URL("/auth/error", getDomain(request)));
}
if (
cookies().get(GMAIL_AUTH_IS_ADMIN_COOKIE_NAME)?.value?.toLowerCase() ===
"true"
) {
return NextResponse.redirect(
new URL("/admin/connectors/gmail", getDomain(request))
);
}
return NextResponse.redirect(new URL("/user/connectors", getDomain(request)));
};

View File

@@ -0,0 +1,265 @@
"use client";
import * as Yup from "yup";
import { GmailIcon } from "@/components/icons/icons";
import useSWR, { useSWRConfig } from "swr";
import { fetcher } from "@/lib/fetcher";
import { LoadingAnimation } from "@/components/Loading";
import { PopupSpec, usePopup } from "@/components/admin/connectors/Popup";
import { HealthCheckBanner } from "@/components/health/healthcheck";
import {
ConnectorIndexingStatus,
Credential,
GmailCredentialJson,
GmailServiceAccountCredentialJson,
GmailConfig,
} from "@/lib/types";
import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm";
import { GmailConnectorsTable } from "./GmailConnectorsTable";
import { gmailConnectorNameBuilder } from "./utils";
import { GmailOAuthSection, GmailJsonUploadSection } from "./Credential";
import { usePublicCredentials } from "@/lib/hooks";
import { AdminPageTitle } from "@/components/admin/Title";
import { Card, Divider, Text, Title } from "@tremor/react";
interface GmailConnectorManagementProps {
gmailPublicCredential?: Credential<GmailCredentialJson>;
gmailServiceAccountCredential?: Credential<GmailServiceAccountCredentialJson>;
gmailConnectorIndexingStatus: ConnectorIndexingStatus<
GmailConfig,
GmailCredentialJson
> | null;
gmailConnectorIndexingStatuses: ConnectorIndexingStatus<
GmailConfig,
GmailCredentialJson
>[];
credentialIsLinked: boolean;
setPopup: (popupSpec: PopupSpec | null) => void;
}
const GmailConnectorManagement = ({
gmailPublicCredential: gmailPublicCredential,
gmailServiceAccountCredential: gmailServiceAccountCredential,
gmailConnectorIndexingStatuses: gmailConnectorIndexingStatuses,
setPopup,
}: GmailConnectorManagementProps) => {
const { mutate } = useSWRConfig();
const liveCredential = gmailPublicCredential || gmailServiceAccountCredential;
if (!liveCredential) {
return (
<Text>
Please authenticate with Gmail as described in Step 2! Once done with
that, you can then move on to enable this connector.
</Text>
);
}
return (
<div>
<Text>
<div className="my-3">
{gmailConnectorIndexingStatuses.length > 0 ? (
<>
Checkout the{" "}
<a href="/admin/indexing/status" className="text-blue-500">
status page
</a>{" "}
for the latest indexing status. We fetch the latest mails from
Gmail every <b>10</b> minutes.
</>
) : (
<p className="text-sm mb-2">
Fill out the form below to create a connector. We will refresh the
latest documents from Gmail every <b>10</b> minutes.
</p>
)}
</div>
</Text>
{gmailConnectorIndexingStatuses.length > 0 && (
<>
<div className="text-sm mb-2 font-bold">Existing Connectors:</div>
<GmailConnectorsTable
gmailConnectorIndexingStatuses={gmailConnectorIndexingStatuses}
setPopup={setPopup}
/>
<Divider />
</>
)}
{gmailConnectorIndexingStatuses.length > 0 && (
<h2 className="font-bold mt-3 text-sm">Add New Connector:</h2>
)}
<Card className="mt-4">
<ConnectorForm<GmailConfig>
nameBuilder={gmailConnectorNameBuilder}
source="gmail"
inputType="poll"
formBody={null}
validationSchema={Yup.object().shape({})}
initialValues={{}}
refreshFreq={10 * 60} // 10 minutes
credentialId={liveCredential.id}
/>
</Card>
</div>
);
};
const Main = () => {
const {
data: appCredentialData,
isLoading: isAppCredentialLoading,
error: isAppCredentialError,
} = useSWR<{ client_id: string }>(
"/api/manage/admin/connector/gmail/app-credential",
fetcher
);
const {
data: serviceAccountKeyData,
isLoading: isServiceAccountKeyLoading,
error: isServiceAccountKeyError,
} = useSWR<{ service_account_email: string }>(
"/api/manage/admin/connector/gmail/service-account-key",
fetcher
);
const {
data: connectorIndexingStatuses,
isLoading: isConnectorIndexingStatusesLoading,
error: isConnectorIndexingStatusesError,
} = useSWR<ConnectorIndexingStatus<any, any>[]>(
"/api/manage/admin/connector/indexing-status",
fetcher
);
const {
data: credentialsData,
isLoading: isCredentialsLoading,
error: isCredentialsError,
refreshCredentials,
} = usePublicCredentials();
const { popup, setPopup } = usePopup();
if (
(!appCredentialData && isAppCredentialLoading) ||
(!serviceAccountKeyData && isServiceAccountKeyLoading) ||
(!connectorIndexingStatuses && isConnectorIndexingStatusesLoading) ||
(!credentialsData && isCredentialsLoading)
) {
return (
<div className="mx-auto">
<LoadingAnimation text="" />
</div>
);
}
if (isCredentialsError || !credentialsData) {
return (
<div className="mx-auto">
<div className="text-red-500">Failed to load credentials.</div>
</div>
);
}
if (isConnectorIndexingStatusesError || !connectorIndexingStatuses) {
return (
<div className="mx-auto">
<div className="text-red-500">Failed to load connectors.</div>
</div>
);
}
if (isAppCredentialError || isServiceAccountKeyError) {
return (
<div className="mx-auto">
<div className="text-red-500">
Error loading Gmail app credentials. Contact an administrator.
</div>
</div>
);
}
const gmailPublicCredential: Credential<GmailCredentialJson> | undefined =
credentialsData.find(
(credential) =>
credential.credential_json?.gmail_tokens && credential.admin_public
);
const gmailServiceAccountCredential:
| Credential<GmailServiceAccountCredentialJson>
| undefined = credentialsData.find(
(credential) => credential.credential_json?.gmail_service_account_key
);
const gmailConnectorIndexingStatuses: ConnectorIndexingStatus<
GmailConfig,
GmailCredentialJson
>[] = connectorIndexingStatuses.filter(
(connectorIndexingStatus) =>
connectorIndexingStatus.connector.source === "gmail"
);
const gmailConnectorIndexingStatus = gmailConnectorIndexingStatuses[0];
const credentialIsLinked =
(gmailConnectorIndexingStatus !== undefined &&
gmailPublicCredential !== undefined &&
gmailConnectorIndexingStatus.connector.credential_ids.includes(
gmailPublicCredential.id
)) ||
(gmailConnectorIndexingStatus !== undefined &&
gmailServiceAccountCredential !== undefined &&
gmailConnectorIndexingStatus.connector.credential_ids.includes(
gmailServiceAccountCredential.id
));
return (
<>
{popup}
<Title className="mb-2 mt-6 ml-auto mr-auto">
Step 1: Provide your Credentials
</Title>
<GmailJsonUploadSection
setPopup={setPopup}
appCredentialData={appCredentialData}
serviceAccountCredentialData={serviceAccountKeyData}
/>
<Title className="mb-2 mt-6 ml-auto mr-auto">
Step 2: Authenticate with Danswer
</Title>
<GmailOAuthSection
setPopup={setPopup}
refreshCredentials={refreshCredentials}
gmailPublicCredential={gmailPublicCredential}
gmailServiceAccountCredential={gmailServiceAccountCredential}
appCredentialData={appCredentialData}
serviceAccountKeyData={serviceAccountKeyData}
connectorExists={gmailConnectorIndexingStatuses.length > 0}
/>
<Title className="mb-2 mt-6 ml-auto mr-auto">
Step 3: Start Indexing!
</Title>
<GmailConnectorManagement
gmailPublicCredential={gmailPublicCredential}
gmailServiceAccountCredential={gmailServiceAccountCredential}
gmailConnectorIndexingStatus={gmailConnectorIndexingStatus}
gmailConnectorIndexingStatuses={gmailConnectorIndexingStatuses}
credentialIsLinked={credentialIsLinked}
setPopup={setPopup}
/>
</>
);
};
export default function Page() {
return (
<div className="mx-auto container">
<div className="mb-4">
<HealthCheckBanner />
</div>
<AdminPageTitle icon={<GmailIcon size={32} />} title="Gmail" />
<Main />
</div>
);
}

View File

@@ -0,0 +1,4 @@
import { GmailConfig } from "@/lib/types";
export const gmailConnectorNameBuilder = (values: GmailConfig) =>
"GmailConnector";

View File

@@ -356,6 +356,20 @@ export const GithubIcon = ({
);
};
export const GmailIcon = ({
size = 16,
className = defaultTailwindCSS,
}: IconProps) => {
return (
<div
style={{ width: `${size}px`, height: `${size}px` }}
className={`w-[${size}px] h-[${size}px] ` + className}
>
<Image src="/Gmail.png" alt="Logo" width="96" height="96" />
</div>
);
};
export const GoogleDriveIcon = ({
size = 16,
className = defaultTailwindCSS,

View File

@@ -4,6 +4,8 @@ export const INTERNAL_URL = process.env.INTERNAL_URL || "http://127.0.0.1:8080";
export const NEXT_PUBLIC_DISABLE_STREAMING =
process.env.NEXT_PUBLIC_DISABLE_STREAMING?.toLowerCase() === "true";
export const GMAIL_AUTH_IS_ADMIN_COOKIE_NAME = "gmail_auth_is_admin";
export const GOOGLE_DRIVE_AUTH_IS_ADMIN_COOKIE_NAME =
"google_drive_auth_is_admin";

41
web/src/lib/gmail.ts Normal file
View File

@@ -0,0 +1,41 @@
import { Credential } from "@/lib/types";
export const setupGmailOAuth = async ({
isAdmin,
}: {
isAdmin: boolean;
}): Promise<[string | null, string]> => {
const credentialCreationResponse = await fetch("/api/manage/credential", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
admin_public: isAdmin,
credential_json: {},
}),
});
if (!credentialCreationResponse.ok) {
return [
null,
`Failed to create credential - ${credentialCreationResponse.status}`,
];
}
const credential =
(await credentialCreationResponse.json()) as Credential<{}>;
const authorizationUrlResponse = await fetch(
`/api/manage/connector/gmail/authorize/${credential.id}`
);
if (!authorizationUrlResponse.ok) {
return [
null,
`Failed to create credential - ${authorizationUrlResponse.status}`,
];
}
const authorizationUrlJson = (await authorizationUrlResponse.json()) as {
auth_url: string;
};
return [authorizationUrlJson.auth_url, ""];
};

View File

@@ -6,6 +6,7 @@ import {
GithubIcon,
GitlabIcon,
GlobeIcon,
GmailIcon,
GongIcon,
GoogleDriveIcon,
GoogleSitesIcon,
@@ -51,6 +52,11 @@ const SOURCE_METADATA_MAP: SourceMap = {
displayName: "Slack",
category: SourceCategory.AppConnection,
},
gmail: {
icon: GmailIcon,
displayName: "Gmail",
category: SourceCategory.AppConnection,
},
google_drive: {
icon: GoogleDriveIcon,
displayName: "Google Drive",

View File

@@ -15,6 +15,7 @@ export type ValidSources =
| "gitlab"
| "slack"
| "google_drive"
| "gmail"
| "bookstack"
| "confluence"
| "jira"
@@ -91,6 +92,8 @@ export interface GoogleDriveConfig {
follow_shortcuts?: boolean;
}
export interface GmailConfig {}
export interface BookstackConfig {}
export interface ConfluenceConfig {
@@ -226,10 +229,19 @@ export interface SlackCredentialJson {
slack_bot_token: string;
}
export interface GmailCredentialJson {
gmail_tokens: string;
}
export interface GoogleDriveCredentialJson {
google_drive_tokens: string;
}
export interface GmailServiceAccountCredentialJson {
gmail_service_account_key: string;
gmail_delegated_user: string;
}
export interface GoogleDriveServiceAccountCredentialJson {
google_drive_service_account_key: string;
google_drive_delegated_user: string;