mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-09-26 11:58:28 +02:00
Gmail Connector (#946)
--------- Co-authored-by: Yuhong Sun <yuhongsun96@gmail.com>
This commit is contained in:
BIN
web/public/Gmail.png
Normal file
BIN
web/public/Gmail.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.0 KiB |
437
web/src/app/admin/connectors/gmail/Credential.tsx
Normal file
437
web/src/app/admin/connectors/gmail/Credential.tsx
Normal 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>'User email to impersonate'</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>
|
||||
);
|
||||
};
|
127
web/src/app/admin/connectors/gmail/GmailConnectorsTable.tsx
Normal file
127
web/src/app/admin/connectors/gmail/GmailConnectorsTable.tsx
Normal 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")
|
||||
}
|
||||
/>
|
||||
),
|
||||
})
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
34
web/src/app/admin/connectors/gmail/auth/callback/route.ts
Normal file
34
web/src/app/admin/connectors/gmail/auth/callback/route.ts
Normal 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)));
|
||||
};
|
265
web/src/app/admin/connectors/gmail/page.tsx
Normal file
265
web/src/app/admin/connectors/gmail/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
4
web/src/app/admin/connectors/gmail/utils.ts
Normal file
4
web/src/app/admin/connectors/gmail/utils.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { GmailConfig } from "@/lib/types";
|
||||
|
||||
export const gmailConnectorNameBuilder = (values: GmailConfig) =>
|
||||
"GmailConnector";
|
@@ -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,
|
||||
|
@@ -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
41
web/src/lib/gmail.ts
Normal 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, ""];
|
||||
};
|
@@ -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",
|
||||
|
@@ -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;
|
||||
|
Reference in New Issue
Block a user