Improved google connector flow (#4155)

* fix handling

* k

* k

* fix function

* k

* k
This commit is contained in:
pablonyx 2025-02-27 21:13:39 -08:00 committed by GitHub
parent 909403a648
commit e80a0f2716
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 627 additions and 281 deletions

View File

@ -360,18 +360,13 @@ def backend_update_credential_json(
db_session.commit()
def delete_credential(
def _delete_credential_internal(
credential: Credential,
credential_id: int,
user: User | None,
db_session: Session,
force: bool = False,
) -> None:
credential = fetch_credential_by_id_for_user(credential_id, user, db_session)
if credential is None:
raise ValueError(
f"Credential by provided id {credential_id} does not exist or does not belong to user"
)
"""Internal utility function to handle the actual deletion of a credential"""
associated_connectors = (
db_session.query(ConnectorCredentialPair)
.filter(ConnectorCredentialPair.credential_id == credential_id)
@ -416,6 +411,35 @@ def delete_credential(
db_session.commit()
def delete_credential_for_user(
credential_id: int,
user: User,
db_session: Session,
force: bool = False,
) -> None:
"""Delete a credential that belongs to a specific user"""
credential = fetch_credential_by_id_for_user(credential_id, user, db_session)
if credential is None:
raise ValueError(
f"Credential by provided id {credential_id} does not exist or does not belong to user"
)
_delete_credential_internal(credential, credential_id, db_session, force)
def delete_credential(
credential_id: int,
db_session: Session,
force: bool = False,
) -> None:
"""Delete a credential regardless of ownership (admin function)"""
credential = fetch_credential_by_id(credential_id, db_session)
if credential is None:
raise ValueError(f"Credential by provided id {credential_id} does not exist")
_delete_credential_internal(credential, credential_id, db_session, force)
def create_initial_public_credential(db_session: Session) -> None:
error_msg = (
"DB is not in a valid initial state."

View File

@ -13,6 +13,7 @@ from onyx.db.credentials import cleanup_gmail_credentials
from onyx.db.credentials import create_credential
from onyx.db.credentials import CREDENTIAL_PERMISSIONS_TO_IGNORE
from onyx.db.credentials import delete_credential
from onyx.db.credentials import delete_credential_for_user
from onyx.db.credentials import fetch_credential_by_id_for_user
from onyx.db.credentials import fetch_credentials_by_source_for_user
from onyx.db.credentials import fetch_credentials_for_user
@ -88,7 +89,7 @@ def delete_credential_by_id_admin(
db_session: Session = Depends(get_session),
) -> StatusResponse:
"""Same as the user endpoint, but can delete any credential (not just the user's own)"""
delete_credential(db_session=db_session, credential_id=credential_id, user=None)
delete_credential(db_session=db_session, credential_id=credential_id)
return StatusResponse(
success=True, message="Credential deleted successfully", data=credential_id
)
@ -242,7 +243,7 @@ def delete_credential_by_id(
user: User = Depends(current_user),
db_session: Session = Depends(get_session),
) -> StatusResponse:
delete_credential(
delete_credential_for_user(
credential_id,
user,
db_session,
@ -259,7 +260,7 @@ def force_delete_credential_by_id(
user: User = Depends(current_user),
db_session: Session = Depends(get_session),
) -> StatusResponse:
delete_credential(credential_id, user, db_session, True)
delete_credential_for_user(credential_id, user, db_session, True)
return StatusResponse(
success=True, message="Credential deleted successfully", data=credential_id

View File

@ -1,6 +1,6 @@
import { Button } from "@/components/Button";
import { PopupSpec } from "@/components/admin/connectors/Popup";
import { useState } from "react";
import React, { useState, useEffect } from "react";
import { useSWRConfig } from "swr";
import * as Yup from "yup";
import { useRouter } from "next/navigation";
@ -17,13 +17,18 @@ import {
GoogleDriveCredentialJson,
GoogleDriveServiceAccountCredentialJson,
} from "@/lib/connectors/credentials";
import { refreshAllGoogleData } from "@/lib/googleConnector";
import { ValidSources } from "@/lib/types";
import { buildSimilarCredentialInfoURL } from "@/app/admin/connector/[ccPairId]/lib";
type GoogleDriveCredentialJsonTypes = "authorized_user" | "service_account";
export const DriveJsonUpload = ({
setPopup,
onSuccess,
}: {
setPopup: (popupSpec: PopupSpec | null) => void;
onSuccess?: () => void;
}) => {
const { mutate } = useSWRConfig();
const [credentialJsonStr, setCredentialJsonStr] = useState<
@ -62,7 +67,6 @@ export const DriveJsonUpload = ({
<Button
disabled={!credentialJsonStr}
onClick={async () => {
// check if the JSON is a app credential or a service account credential
let credentialFileType: GoogleDriveCredentialJsonTypes;
try {
const appCredentialJson = JSON.parse(credentialJsonStr!);
@ -99,6 +103,10 @@ export const DriveJsonUpload = ({
message: "Successfully uploaded app credentials",
type: "success",
});
mutate("/api/manage/admin/connector/google-drive/app-credential");
if (onSuccess) {
onSuccess();
}
} else {
const errorMsg = await response.text();
setPopup({
@ -106,7 +114,6 @@ export const DriveJsonUpload = ({
type: "error",
});
}
mutate("/api/manage/admin/connector/google-drive/app-credential");
}
if (credentialFileType === "service_account") {
@ -122,19 +129,22 @@ export const DriveJsonUpload = ({
);
if (response.ok) {
setPopup({
message: "Successfully uploaded app credentials",
message: "Successfully uploaded service account key",
type: "success",
});
mutate(
"/api/manage/admin/connector/google-drive/service-account-key"
);
if (onSuccess) {
onSuccess();
}
} else {
const errorMsg = await response.text();
setPopup({
message: `Failed to upload app credentials - ${errorMsg}`,
message: `Failed to upload service account key - ${errorMsg}`,
type: "error",
});
}
mutate(
"/api/manage/admin/connector/google-drive/service-account-key"
);
}
}}
>
@ -149,6 +159,7 @@ interface DriveJsonUploadSectionProps {
appCredentialData?: { client_id: string };
serviceAccountCredentialData?: { service_account_email: string };
isAdmin: boolean;
onSuccess?: () => void;
}
export const DriveJsonUploadSection = ({
@ -156,17 +167,36 @@ export const DriveJsonUploadSection = ({
appCredentialData,
serviceAccountCredentialData,
isAdmin,
onSuccess,
}: DriveJsonUploadSectionProps) => {
const { mutate } = useSWRConfig();
const router = useRouter();
const [localServiceAccountData, setLocalServiceAccountData] = useState(
serviceAccountCredentialData
);
const [localAppCredentialData, setLocalAppCredentialData] =
useState(appCredentialData);
if (serviceAccountCredentialData?.service_account_email) {
useEffect(() => {
setLocalServiceAccountData(serviceAccountCredentialData);
setLocalAppCredentialData(appCredentialData);
}, [serviceAccountCredentialData, appCredentialData]);
const handleSuccess = () => {
if (onSuccess) {
onSuccess();
} else {
refreshAllGoogleData(ValidSources.GoogleDrive);
}
};
if (localServiceAccountData?.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}
{localServiceAccountData.service_account_email}
</p>
</div>
{isAdmin ? (
@ -188,11 +218,15 @@ export const DriveJsonUploadSection = ({
mutate(
"/api/manage/admin/connector/google-drive/service-account-key"
);
mutate(
buildSimilarCredentialInfoURL(ValidSources.GoogleDrive)
);
setPopup({
message: "Successfully deleted service account key",
type: "success",
});
router.refresh();
setLocalServiceAccountData(undefined);
handleSuccess();
} else {
const errorMsg = await response.text();
setPopup({
@ -216,12 +250,12 @@ export const DriveJsonUploadSection = ({
);
}
if (appCredentialData?.client_id) {
if (localAppCredentialData?.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>
<p className="italic mt-1">{localAppCredentialData.client_id}</p>
</div>
{isAdmin ? (
<>
@ -242,10 +276,15 @@ export const DriveJsonUploadSection = ({
mutate(
"/api/manage/admin/connector/google-drive/app-credential"
);
mutate(
buildSimilarCredentialInfoURL(ValidSources.GoogleDrive)
);
setPopup({
message: "Successfully deleted app credentials",
type: "success",
});
setLocalAppCredentialData(undefined);
handleSuccess();
} else {
const errorMsg = await response.text();
setPopup({
@ -297,7 +336,7 @@ export const DriveJsonUploadSection = ({
Download the credentials JSON if choosing option (1) or the Service
Account key JSON if chooosing option (2), and upload it here.
</p>
<DriveJsonUpload setPopup={setPopup} />
<DriveJsonUpload setPopup={setPopup} onSuccess={handleSuccess} />
</div>
);
};
@ -348,13 +387,41 @@ export const DriveAuthSection = ({
appCredentialData,
setPopup,
refreshCredentials,
connectorAssociated, // don't allow revoke if a connector / credential pair is active with the uploaded credential
connectorAssociated,
user,
}: DriveCredentialSectionProps) => {
const router = useRouter();
const [localServiceAccountData, setLocalServiceAccountData] = useState(
serviceAccountKeyData
);
const [localAppCredentialData, setLocalAppCredentialData] =
useState(appCredentialData);
const [
localGoogleDrivePublicCredential,
setLocalGoogleDrivePublicCredential,
] = useState(googleDrivePublicUploadedCredential);
const [
localGoogleDriveServiceAccountCredential,
setLocalGoogleDriveServiceAccountCredential,
] = useState(googleDriveServiceAccountCredential);
useEffect(() => {
setLocalServiceAccountData(serviceAccountKeyData);
setLocalAppCredentialData(appCredentialData);
setLocalGoogleDrivePublicCredential(googleDrivePublicUploadedCredential);
setLocalGoogleDriveServiceAccountCredential(
googleDriveServiceAccountCredential
);
}, [
serviceAccountKeyData,
appCredentialData,
googleDrivePublicUploadedCredential,
googleDriveServiceAccountCredential,
]);
const existingCredential =
googleDrivePublicUploadedCredential || googleDriveServiceAccountCredential;
localGoogleDrivePublicCredential ||
localGoogleDriveServiceAccountCredential;
if (existingCredential) {
return (
<>
@ -377,7 +444,7 @@ export const DriveAuthSection = ({
);
}
if (serviceAccountKeyData?.service_account_email) {
if (localServiceAccountData?.service_account_email) {
return (
<div>
<Formik
@ -438,7 +505,7 @@ export const DriveAuthSection = ({
);
}
if (appCredentialData?.client_id) {
if (localAppCredentialData?.client_id) {
return (
<div className="text-sm mb-4">
<p className="mb-2">

View File

@ -1,8 +1,7 @@
"use client";
import React, { useEffect, useState } from "react";
import useSWR, { mutate } from "swr";
import { FetchError, errorHandlingFetcher } from "@/lib/fetcher";
import React from "react";
import { FetchError } from "@/lib/fetcher";
import { ErrorCallout } from "@/components/ErrorCallout";
import { LoadingAnimation } from "@/components/Loading";
import { PopupSpec, usePopup } from "@/components/admin/connectors/Popup";
@ -15,22 +14,17 @@ import {
GoogleDriveCredentialJson,
GoogleDriveServiceAccountCredentialJson,
} from "@/lib/connectors/credentials";
import { ConnectorSnapshot } from "@/lib/connectors/connectors";
import { useUser } from "@/components/user/UserProvider";
import { buildSimilarCredentialInfoURL } from "@/app/admin/connector/[ccPairId]/lib";
const useConnectorsByCredentialId = (credential_id: number | null) => {
let url: string | null = null;
if (credential_id !== null) {
url = `/api/manage/admin/connector?credential=${credential_id}`;
}
const swrResponse = useSWR<ConnectorSnapshot[]>(url, errorHandlingFetcher);
return {
...swrResponse,
refreshConnectorsByCredentialId: () => mutate(url),
};
};
import {
useGoogleAppCredential,
useGoogleServiceAccountKey,
useGoogleCredentials,
useConnectorsByCredentialId,
checkCredentialsFetched,
filterUploadedCredentials,
checkConnectorsExist,
refreshAllGoogleData,
} from "@/lib/googleConnector";
const GDriveMain = ({
setPopup,
@ -39,27 +33,20 @@ const GDriveMain = ({
}) => {
const { isAdmin, user } = useUser();
// tries getting the uploaded credential json
// Get app credential and service account key
const {
data: appCredentialData,
isLoading: isAppCredentialLoading,
error: isAppCredentialError,
} = useSWR<{ client_id: string }, FetchError>(
"/api/manage/admin/connector/google-drive/app-credential",
errorHandlingFetcher
);
} = useGoogleAppCredential("google_drive");
// tries getting the uploaded service account key
const {
data: serviceAccountKeyData,
isLoading: isServiceAccountKeyLoading,
error: isServiceAccountKeyError,
} = useSWR<{ service_account_email: string }, FetchError>(
"/api/manage/admin/connector/google-drive/service-account-key",
errorHandlingFetcher
);
} = useGoogleServiceAccountKey("google_drive");
// gets all public credentials
// Get all public credentials
const {
data: credentialsData,
isLoading: isCredentialsLoading,
@ -67,33 +54,19 @@ const GDriveMain = ({
refreshCredentials,
} = usePublicCredentials();
// gets all credentials for source type google drive
// Get Google Drive-specific credentials
const {
data: googleDriveCredentials,
isLoading: isGoogleDriveCredentialsLoading,
error: googleDriveCredentialsError,
} = useSWR<Credential<any>[]>(
buildSimilarCredentialInfoURL(ValidSources.GoogleDrive),
errorHandlingFetcher,
{ refreshInterval: 5000 }
} = useGoogleCredentials(ValidSources.GoogleDrive);
// Filter uploaded credentials and get credential ID
const { credential_id, uploadedCredentials } = filterUploadedCredentials(
googleDriveCredentials
);
// filters down to just credentials that were created via upload (there should be only one)
let credential_id = null;
if (googleDriveCredentials) {
const googleDriveUploadedCredentials: Credential<GoogleDriveCredentialJson>[] =
googleDriveCredentials.filter(
(googleDriveCredential) =>
googleDriveCredential.credential_json.authentication_method !==
"oauth_interactive"
);
if (googleDriveUploadedCredentials.length > 0) {
credential_id = googleDriveUploadedCredentials[0].id;
}
}
// retrieves all connectors for that credential id
// Get connectors for the credential ID
const {
data: googleDriveConnectors,
isLoading: isGoogleDriveConnectorsLoading,
@ -101,13 +74,25 @@ const GDriveMain = ({
refreshConnectorsByCredentialId,
} = useConnectorsByCredentialId(credential_id);
const appCredentialSuccessfullyFetched =
appCredentialData ||
(isAppCredentialError && isAppCredentialError.status === 404);
const serviceAccountKeySuccessfullyFetched =
serviceAccountKeyData ||
(isServiceAccountKeyError && isServiceAccountKeyError.status === 404);
// Check if credentials were successfully fetched
const {
appCredentialSuccessfullyFetched,
serviceAccountKeySuccessfullyFetched,
} = checkCredentialsFetched(
appCredentialData,
isAppCredentialError,
serviceAccountKeyData,
isServiceAccountKeyError
);
// Handle refresh of all data
const handleRefresh = () => {
refreshCredentials();
refreshConnectorsByCredentialId();
refreshAllGoogleData(ValidSources.GoogleDrive);
};
// Loading state
if (
(!appCredentialSuccessfullyFetched && isAppCredentialLoading) ||
(!serviceAccountKeySuccessfullyFetched && isServiceAccountKeyLoading) ||
@ -122,6 +107,7 @@ const GDriveMain = ({
);
}
// Error states
if (credentialsError || !credentialsData) {
return <ErrorCallout errorTitle="Failed to load credentials." />;
}
@ -141,7 +127,16 @@ const GDriveMain = ({
);
}
// get the actual uploaded oauth or service account credentials
if (googleDriveConnectorsError) {
return (
<ErrorCallout errorTitle="Failed to load Google Drive associated connectors." />
);
}
// Check if connectors exist
const connectorAssociated = checkConnectorsExist(googleDriveConnectors);
// Get the uploaded OAuth credential
const googleDrivePublicUploadedCredential:
| Credential<GoogleDriveCredentialJson>
| undefined = credentialsData.find(
@ -152,6 +147,7 @@ const GDriveMain = ({
credential.credential_json.authentication_method !== "oauth_interactive"
);
// Get the service account credential
const googleDriveServiceAccountCredential:
| Credential<GoogleDriveServiceAccountCredentialJson>
| undefined = credentialsData.find(
@ -160,19 +156,6 @@ const GDriveMain = ({
credential.source === "google_drive"
);
if (googleDriveConnectorsError) {
return (
<ErrorCallout errorTitle="Failed to load Google Drive associated connectors." />
);
}
let connectorAssociated = false;
if (googleDriveConnectors) {
if (googleDriveConnectors.length > 0) {
connectorAssociated = true;
}
}
return (
<>
<Title className="mb-2 mt-6">Step 1: Provide your Credentials</Title>
@ -181,27 +164,30 @@ const GDriveMain = ({
appCredentialData={appCredentialData}
serviceAccountCredentialData={serviceAccountKeyData}
isAdmin={isAdmin}
onSuccess={handleRefresh}
/>
{isAdmin && (
<>
<Title className="mb-2 mt-6">Step 2: Authenticate with Onyx</Title>
<DriveAuthSection
setPopup={setPopup}
refreshCredentials={refreshCredentials}
googleDrivePublicUploadedCredential={
googleDrivePublicUploadedCredential
}
googleDriveServiceAccountCredential={
googleDriveServiceAccountCredential
}
appCredentialData={appCredentialData}
serviceAccountKeyData={serviceAccountKeyData}
connectorAssociated={connectorAssociated}
user={user}
/>
</>
)}
{isAdmin &&
(appCredentialData?.client_id ||
serviceAccountKeyData?.service_account_email) && (
<>
<Title className="mb-2 mt-6">Step 2: Authenticate with Onyx</Title>
<DriveAuthSection
setPopup={setPopup}
refreshCredentials={handleRefresh}
googleDrivePublicUploadedCredential={
googleDrivePublicUploadedCredential
}
googleDriveServiceAccountCredential={
googleDriveServiceAccountCredential
}
appCredentialData={appCredentialData}
serviceAccountKeyData={serviceAccountKeyData}
connectorAssociated={connectorAssociated}
user={user}
/>
</>
)}
</>
);
};

View File

@ -1,6 +1,6 @@
import { Button } from "@/components/Button";
import { PopupSpec } from "@/components/admin/connectors/Popup";
import { useState } from "react";
import React, { useState, useEffect } from "react";
import { useSWRConfig } from "swr";
import * as Yup from "yup";
import { useRouter } from "next/navigation";
@ -17,13 +17,18 @@ import {
GmailCredentialJson,
GmailServiceAccountCredentialJson,
} from "@/lib/connectors/credentials";
import { refreshAllGoogleData } from "@/lib/googleConnector";
import { ValidSources } from "@/lib/types";
import { buildSimilarCredentialInfoURL } from "@/app/admin/connector/[ccPairId]/lib";
type GmailCredentialJsonTypes = "authorized_user" | "service_account";
const DriveJsonUpload = ({
setPopup,
onSuccess,
}: {
setPopup: (popupSpec: PopupSpec | null) => void;
onSuccess?: () => void;
}) => {
const { mutate } = useSWRConfig();
const [credentialJsonStr, setCredentialJsonStr] = useState<
@ -72,7 +77,7 @@ const DriveJsonUpload = ({
credentialFileType = "service_account";
} else {
throw new Error(
"Unknown credential type, expected 'OAuth Web application'"
"Unknown credential type, expected one of 'OAuth Web application' or 'Service Account'"
);
}
} catch (e) {
@ -99,6 +104,10 @@ const DriveJsonUpload = ({
message: "Successfully uploaded app credentials",
type: "success",
});
mutate("/api/manage/admin/connector/gmail/app-credential");
if (onSuccess) {
onSuccess();
}
} else {
const errorMsg = await response.text();
setPopup({
@ -106,7 +115,6 @@ const DriveJsonUpload = ({
type: "error",
});
}
mutate("/api/manage/admin/connector/gmail/app-credential");
}
if (credentialFileType === "service_account") {
@ -122,17 +130,20 @@ const DriveJsonUpload = ({
);
if (response.ok) {
setPopup({
message: "Successfully uploaded app credentials",
message: "Successfully uploaded service account key",
type: "success",
});
mutate("/api/manage/admin/connector/gmail/service-account-key");
if (onSuccess) {
onSuccess();
}
} else {
const errorMsg = await response.text();
setPopup({
message: `Failed to upload app credentials - ${errorMsg}`,
message: `Failed to upload service account key - ${errorMsg}`,
type: "error",
});
}
mutate("/api/manage/admin/connector/gmail/service-account-key");
}
}}
>
@ -147,6 +158,7 @@ interface DriveJsonUploadSectionProps {
appCredentialData?: { client_id: string };
serviceAccountCredentialData?: { service_account_email: string };
isAdmin: boolean;
onSuccess?: () => void;
}
export const GmailJsonUploadSection = ({
@ -154,16 +166,37 @@ export const GmailJsonUploadSection = ({
appCredentialData,
serviceAccountCredentialData,
isAdmin,
onSuccess,
}: DriveJsonUploadSectionProps) => {
const { mutate } = useSWRConfig();
const router = useRouter();
const [localServiceAccountData, setLocalServiceAccountData] = useState(
serviceAccountCredentialData
);
const [localAppCredentialData, setLocalAppCredentialData] =
useState(appCredentialData);
if (serviceAccountCredentialData?.service_account_email) {
// Update local state when props change
useEffect(() => {
setLocalServiceAccountData(serviceAccountCredentialData);
setLocalAppCredentialData(appCredentialData);
}, [serviceAccountCredentialData, appCredentialData]);
const handleSuccess = () => {
if (onSuccess) {
onSuccess();
} else {
refreshAllGoogleData(ValidSources.Gmail);
}
};
if (localServiceAccountData?.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}
{localServiceAccountData.service_account_email}
</p>
</div>
{isAdmin ? (
@ -185,10 +218,15 @@ export const GmailJsonUploadSection = ({
mutate(
"/api/manage/admin/connector/gmail/service-account-key"
);
// Also mutate the credential endpoints to ensure Step 2 is reset
mutate(buildSimilarCredentialInfoURL(ValidSources.Gmail));
setPopup({
message: "Successfully deleted service account key",
type: "success",
});
// Immediately update local state
setLocalServiceAccountData(undefined);
handleSuccess();
} else {
const errorMsg = await response.text();
setPopup({
@ -212,43 +250,56 @@ export const GmailJsonUploadSection = ({
);
}
if (appCredentialData?.client_id) {
if (localAppCredentialData?.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>
<p className="italic mt-1">{localAppCredentialData.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>
{isAdmin ? (
<>
<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");
// Also mutate the credential endpoints to ensure Step 2 is reset
mutate(buildSimilarCredentialInfoURL(ValidSources.Gmail));
setPopup({
message: "Successfully deleted app credentials",
type: "success",
});
// Immediately update local state
setLocalAppCredentialData(undefined);
handleSuccess();
} else {
const errorMsg = await response.text();
setPopup({
message: `Failed to delete app credential - ${errorMsg}`,
type: "error",
});
}
}}
>
Delete
</Button>
</>
) : (
<div className="mt-4 mb-1">
To change these credentials, please contact an administrator.
</div>
)}
</div>
);
}
@ -276,14 +327,14 @@ export const GmailJsonUploadSection = ({
>
here
</a>{" "}
to either (1) setup a google OAuth App in your company workspace or (2)
to either (1) setup a Google OAuth App in your company workspace or (2)
create a Service Account.
<br />
<br />
Download the credentials JSON if choosing option (1) or the Service
Account key JSON if chooosing option (2), and upload it here.
Account key JSON if choosing option (2), and upload it here.
</p>
<DriveJsonUpload setPopup={setPopup} />
<DriveJsonUpload setPopup={setPopup} onSuccess={handleSuccess} />
</div>
);
};
@ -299,6 +350,34 @@ interface DriveCredentialSectionProps {
user: User | null;
}
async function handleRevokeAccess(
connectorExists: boolean,
setPopup: (popupSpec: PopupSpec | null) => void,
existingCredential:
| Credential<GmailCredentialJson>
| Credential<GmailServiceAccountCredentialJson>,
refreshCredentials: () => void
) {
if (connectorExists) {
const message =
"Cannot revoke the Gmail credential while any connector is still associated with the credential. " +
"Please delete all associated connectors, then try again.";
setPopup({
message: message,
type: "error",
});
return;
}
await adminDeleteCredential(existingCredential.id);
setPopup({
message: "Successfully revoked the Gmail credential!",
type: "success",
});
refreshCredentials();
}
export const GmailAuthSection = ({
gmailPublicCredential,
gmailServiceAccountCredential,
@ -310,31 +389,49 @@ export const GmailAuthSection = ({
user,
}: DriveCredentialSectionProps) => {
const router = useRouter();
const [isAuthenticating, setIsAuthenticating] = useState(false);
const [localServiceAccountData, setLocalServiceAccountData] = useState(
serviceAccountKeyData
);
const [localAppCredentialData, setLocalAppCredentialData] =
useState(appCredentialData);
const [localGmailPublicCredential, setLocalGmailPublicCredential] = useState(
gmailPublicCredential
);
const [
localGmailServiceAccountCredential,
setLocalGmailServiceAccountCredential,
] = useState(gmailServiceAccountCredential);
// Update local state when props change
useEffect(() => {
setLocalServiceAccountData(serviceAccountKeyData);
setLocalAppCredentialData(appCredentialData);
setLocalGmailPublicCredential(gmailPublicCredential);
setLocalGmailServiceAccountCredential(gmailServiceAccountCredential);
}, [
serviceAccountKeyData,
appCredentialData,
gmailPublicCredential,
gmailServiceAccountCredential,
]);
const existingCredential =
gmailPublicCredential || gmailServiceAccountCredential;
localGmailPublicCredential || localGmailServiceAccountCredential;
if (existingCredential) {
return (
<>
<p className="mb-2 text-sm">
<i>Existing credential already set up!</i>
<i>Uploaded and authenticated credential already exists!</i>
</p>
<Button
onClick={async () => {
if (connectorExists) {
setPopup({
message:
"Cannot revoke access to Gmail while any connector is still set up. Please delete all connectors, then try again.",
type: "error",
});
return;
}
await adminDeleteCredential(existingCredential.id);
setPopup({
message: "Successfully revoked access to Gmail!",
type: "success",
});
refreshCredentials();
handleRevokeAccess(
connectorExists,
setPopup,
existingCredential,
refreshCredentials
);
}}
>
Revoke Access
@ -343,20 +440,21 @@ export const GmailAuthSection = ({
);
}
if (serviceAccountKeyData?.service_account_email) {
if (localServiceAccountData?.service_account_email) {
return (
<div>
<CardSection>
<Formik
initialValues={{
google_primary_admin: user?.email || "",
}}
validationSchema={Yup.object().shape({
google_primary_admin: Yup.string().required(),
})}
onSubmit={async (values, formikHelpers) => {
formikHelpers.setSubmitting(true);
<Formik
initialValues={{
google_primary_admin: user?.email || "",
}}
validationSchema={Yup.object().shape({
google_primary_admin: Yup.string()
.email("Must be a valid email")
.required("Required"),
})}
onSubmit={async (values, formikHelpers) => {
formikHelpers.setSubmitting(true);
try {
const response = await fetch(
"/api/manage/admin/connector/gmail/service-account-credential",
{
@ -375,6 +473,7 @@ export const GmailAuthSection = ({
message: "Successfully created service account credential",
type: "success",
});
refreshCredentials();
} else {
const errorMsg = await response.text();
setPopup({
@ -382,65 +481,73 @@ export const GmailAuthSection = ({
type: "error",
});
}
refreshCredentials();
}}
>
{({ isSubmitting }) => (
<Form>
<TextFormField
name="google_primary_admin"
label="Primary Admin Email:"
subtext="You must provide an admin/owner account to retrieve all org emails."
/>
<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>
</CardSection>
} catch (error) {
setPopup({
message: `Failed to create service account credential - ${error}`,
type: "error",
});
} finally {
formikHelpers.setSubmitting(false);
}
}}
>
{({ isSubmitting }) => (
<Form>
<TextFormField
name="google_primary_admin"
label="Primary Admin Email:"
subtext="Enter the email of an admin/owner of the Google Organization that owns the Gmail account(s) you want to index."
/>
<div className="flex">
<Button type="submit" disabled={isSubmitting}>
Create Credential
</Button>
</div>
</Form>
)}
</Formik>
</div>
);
}
if (appCredentialData?.client_id) {
if (localAppCredentialData?.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.
access to the emails 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
setIsAuthenticating(true);
try {
Cookies.set(GMAIL_AUTH_IS_ADMIN_COOKIE_NAME, "true", {
path: "/",
});
router.push(authUrl);
return;
}
const [authUrl, errorMsg] = await setupGmailOAuth({
isAdmin: true,
});
setPopup({
message: errorMsg,
type: "error",
});
if (authUrl) {
router.push(authUrl);
} else {
setPopup({
message: errorMsg,
type: "error",
});
setIsAuthenticating(false);
}
} catch (error) {
setPopup({
message: `Failed to authenticate with Gmail - ${error}`,
type: "error",
});
setIsAuthenticating(false);
}
}}
disabled={isAuthenticating}
>
Authenticate with Gmail
{isAuthenticating ? "Authenticating..." : "Authenticate with Gmail"}
</Button>
</div>
);
@ -449,8 +556,8 @@ export const GmailAuthSection = ({
// case where no keys have been uploaded in step 1
return (
<p className="text-sm">
Please upload an OAuth or Service Account Credential JSON in Step 1 before
moving onto Step 2.
Please upload either a OAuth Client Credential JSON or a Gmail Service
Account Key JSON in Step 1 before moving onto Step 2.
</p>
);
};

View File

@ -1,10 +1,11 @@
"use client";
import useSWR from "swr";
import { errorHandlingFetcher } from "@/lib/fetcher";
import React from "react";
import { FetchError } from "@/lib/fetcher";
import { ErrorCallout } from "@/components/ErrorCallout";
import { LoadingAnimation } from "@/components/Loading";
import { usePopup } from "@/components/admin/connectors/Popup";
import { CCPairBasicInfo } from "@/lib/types";
import { PopupSpec, usePopup } from "@/components/admin/connectors/Popup";
import { CCPairBasicInfo, ValidSources } from "@/lib/types";
import {
Credential,
GmailCredentialJson,
@ -14,26 +15,33 @@ import { GmailAuthSection, GmailJsonUploadSection } from "./Credential";
import { usePublicCredentials, useBasicConnectorStatus } from "@/lib/hooks";
import Title from "@/components/ui/title";
import { useUser } from "@/components/user/UserProvider";
import {
useGoogleAppCredential,
useGoogleServiceAccountKey,
useGoogleCredentials,
useConnectorsByCredentialId,
checkCredentialsFetched,
filterUploadedCredentials,
checkConnectorsExist,
refreshAllGoogleData,
} from "@/lib/googleConnector";
export const GmailMain = () => {
const { isAdmin, user } = useUser();
const { popup, setPopup } = usePopup();
const {
data: appCredentialData,
isLoading: isAppCredentialLoading,
error: isAppCredentialError,
} = useSWR<{ client_id: string }>(
"/api/manage/admin/connector/gmail/app-credential",
errorHandlingFetcher
);
} = useGoogleAppCredential("gmail");
const {
data: serviceAccountKeyData,
isLoading: isServiceAccountKeyLoading,
error: isServiceAccountKeyError,
} = useSWR<{ service_account_email: string }>(
"/api/manage/admin/connector/gmail/service-account-key",
errorHandlingFetcher
);
} = useGoogleServiceAccountKey("gmail");
const {
data: connectorIndexingStatuses,
isLoading: isConnectorIndexingStatusesLoading,
@ -47,20 +55,45 @@ export const GmailMain = () => {
refreshCredentials,
} = usePublicCredentials();
const { popup, setPopup } = usePopup();
const {
data: gmailCredentials,
isLoading: isGmailCredentialsLoading,
error: gmailCredentialsError,
} = useGoogleCredentials(ValidSources.Gmail);
const appCredentialSuccessfullyFetched =
appCredentialData ||
(isAppCredentialError && isAppCredentialError.status === 404);
const serviceAccountKeySuccessfullyFetched =
serviceAccountKeyData ||
(isServiceAccountKeyError && isServiceAccountKeyError.status === 404);
const { credential_id, uploadedCredentials } =
filterUploadedCredentials(gmailCredentials);
const {
data: gmailConnectors,
isLoading: isGmailConnectorsLoading,
error: gmailConnectorsError,
refreshConnectorsByCredentialId,
} = useConnectorsByCredentialId(credential_id);
const {
appCredentialSuccessfullyFetched,
serviceAccountKeySuccessfullyFetched,
} = checkCredentialsFetched(
appCredentialData,
isAppCredentialError,
serviceAccountKeyData,
isServiceAccountKeyError
);
const handleRefresh = () => {
refreshCredentials();
refreshConnectorsByCredentialId();
refreshAllGoogleData(ValidSources.Gmail);
};
if (
(!appCredentialSuccessfullyFetched && isAppCredentialLoading) ||
(!serviceAccountKeySuccessfullyFetched && isServiceAccountKeyLoading) ||
(!connectorIndexingStatuses && isConnectorIndexingStatusesLoading) ||
(!credentialsData && isCredentialsLoading)
(!credentialsData && isCredentialsLoading) ||
(!gmailCredentials && isGmailCredentialsLoading) ||
(!gmailConnectors && isGmailConnectorsLoading)
) {
return (
<div className="mx-auto">
@ -70,19 +103,15 @@ export const GmailMain = () => {
}
if (credentialsError || !credentialsData) {
return (
<div className="mx-auto">
<div className="text-red-500">Failed to load credentials.</div>
</div>
);
return <ErrorCallout errorTitle="Failed to load credentials." />;
}
if (gmailCredentialsError || !gmailCredentials) {
return <ErrorCallout errorTitle="Failed to load Gmail credentials." />;
}
if (connectorIndexingStatusesError || !connectorIndexingStatuses) {
return (
<div className="mx-auto">
<div className="text-red-500">Failed to load connectors.</div>
</div>
);
return <ErrorCallout errorTitle="Failed to load connectors." />;
}
if (
@ -90,21 +119,28 @@ export const GmailMain = () => {
!serviceAccountKeySuccessfullyFetched
) {
return (
<div className="mx-auto">
<div className="text-red-500">
Error loading Gmail app credentials. Contact an administrator.
</div>
</div>
<ErrorCallout errorTitle="Error loading Gmail app credentials. Contact an administrator." />
);
}
const gmailPublicCredential: Credential<GmailCredentialJson> | undefined =
credentialsData.find(
(credential) =>
(credential.credential_json?.google_service_account_key ||
credential.credential_json?.google_tokens) &&
credential.admin_public
if (gmailConnectorsError) {
return (
<ErrorCallout errorTitle="Failed to load Gmail associated connectors." />
);
}
const connectorExistsFromCredential = checkConnectorsExist(gmailConnectors);
const gmailPublicUploadedCredential:
| Credential<GmailCredentialJson>
| undefined = credentialsData.find(
(credential) =>
credential.credential_json?.google_tokens &&
credential.admin_public &&
credential.source === "gmail" &&
credential.credential_json.authentication_method !== "oauth_interactive"
);
const gmailServiceAccountCredential:
| Credential<GmailServiceAccountCredentialJson>
| undefined = credentialsData.find(
@ -118,6 +154,13 @@ export const GmailMain = () => {
(connectorIndexingStatus) => connectorIndexingStatus.source === "gmail"
);
const connectorExists =
connectorExistsFromCredential || gmailConnectorIndexingStatuses.length > 0;
const hasUploadedCredentials =
Boolean(appCredentialData?.client_id) ||
Boolean(serviceAccountKeyData?.service_account_email);
return (
<>
{popup}
@ -129,21 +172,22 @@ export const GmailMain = () => {
appCredentialData={appCredentialData}
serviceAccountCredentialData={serviceAccountKeyData}
isAdmin={isAdmin}
onSuccess={handleRefresh}
/>
{isAdmin && (
{isAdmin && hasUploadedCredentials && (
<>
<Title className="mb-2 mt-6 ml-auto mr-auto">
Step 2: Authenticate with Onyx
</Title>
<GmailAuthSection
setPopup={setPopup}
refreshCredentials={refreshCredentials}
gmailPublicCredential={gmailPublicCredential}
refreshCredentials={handleRefresh}
gmailPublicCredential={gmailPublicUploadedCredential}
gmailServiceAccountCredential={gmailServiceAccountCredential}
appCredentialData={appCredentialData}
serviceAccountKeyData={serviceAccountKeyData}
connectorExists={gmailConnectorIndexingStatuses.length > 0}
connectorExists={connectorExists}
user={user}
/>
</>

View File

@ -3,7 +3,6 @@ interface Props {
onClick?: React.MouseEventHandler<HTMLButtonElement>;
type?: "button" | "submit" | "reset";
disabled?: boolean;
fullWidth?: boolean;
className?: string;
}
@ -12,14 +11,12 @@ export const Button = ({
onClick,
type = "submit",
disabled = false,
fullWidth = false,
className = "",
}: Props) => {
return (
<button
className={
"group relative " +
(fullWidth ? "w-full " : "") +
"py-1 px-2 border border-transparent text-sm " +
"font-medium rounded-md text-white " +
"focus:outline-none focus:ring-2 " +

View File

@ -0,0 +1,120 @@
import useSWR, { mutate } from "swr";
import { FetchError, errorHandlingFetcher } from "@/lib/fetcher";
import { Credential } from "@/lib/connectors/credentials";
import { ConnectorSnapshot } from "@/lib/connectors/connectors";
import { ValidSources } from "@/lib/types";
import { buildSimilarCredentialInfoURL } from "@/app/admin/connector/[ccPairId]/lib";
// Constants for service names to avoid typos
export const GOOGLE_SERVICES = {
GMAIL: "gmail",
GOOGLE_DRIVE: "google-drive",
} as const;
export const useGoogleAppCredential = (service: "gmail" | "google_drive") => {
const endpoint = `/api/manage/admin/connector/${
service === "gmail" ? GOOGLE_SERVICES.GMAIL : GOOGLE_SERVICES.GOOGLE_DRIVE
}/app-credential`;
return useSWR<{ client_id: string }, FetchError>(
endpoint,
errorHandlingFetcher
);
};
export const useGoogleServiceAccountKey = (
service: "gmail" | "google_drive"
) => {
const endpoint = `/api/manage/admin/connector/${
service === "gmail" ? GOOGLE_SERVICES.GMAIL : GOOGLE_SERVICES.GOOGLE_DRIVE
}/service-account-key`;
return useSWR<{ service_account_email: string }, FetchError>(
endpoint,
errorHandlingFetcher
);
};
export const useGoogleCredentials = (
source: ValidSources.Gmail | ValidSources.GoogleDrive
) => {
return useSWR<Credential<any>[]>(
buildSimilarCredentialInfoURL(source),
errorHandlingFetcher,
{ refreshInterval: 5000 }
);
};
export const useConnectorsByCredentialId = (credential_id: number | null) => {
let url: string | null = null;
if (credential_id !== null) {
url = `/api/manage/admin/connector?credential=${credential_id}`;
}
const swrResponse = useSWR<ConnectorSnapshot[]>(url, errorHandlingFetcher);
return {
...swrResponse,
refreshConnectorsByCredentialId: () => mutate(url),
};
};
export const checkCredentialsFetched = (
appCredentialData: any,
appCredentialError: FetchError | undefined,
serviceAccountKeyData: any,
serviceAccountKeyError: FetchError | undefined
) => {
const appCredentialSuccessfullyFetched =
appCredentialData ||
(appCredentialError && appCredentialError.status === 404);
const serviceAccountKeySuccessfullyFetched =
serviceAccountKeyData ||
(serviceAccountKeyError && serviceAccountKeyError.status === 404);
return {
appCredentialSuccessfullyFetched,
serviceAccountKeySuccessfullyFetched,
};
};
export const filterUploadedCredentials = <
T extends { authentication_method?: string },
>(
credentials: Credential<T>[] | undefined
): { credential_id: number | null; uploadedCredentials: Credential<T>[] } => {
let credential_id = null;
let uploadedCredentials: Credential<T>[] = [];
if (credentials) {
uploadedCredentials = credentials.filter(
(credential) =>
credential.credential_json.authentication_method !== "oauth_interactive"
);
if (uploadedCredentials.length > 0) {
credential_id = uploadedCredentials[0].id;
}
}
return { credential_id, uploadedCredentials };
};
export const checkConnectorsExist = (
connectors: ConnectorSnapshot[] | undefined
): boolean => {
return !!connectors && connectors.length > 0;
};
export const refreshAllGoogleData = (
source: ValidSources.Gmail | ValidSources.GoogleDrive
) => {
mutate(buildSimilarCredentialInfoURL(source));
const service =
source === ValidSources.Gmail
? GOOGLE_SERVICES.GMAIL
: GOOGLE_SERVICES.GOOGLE_DRIVE;
mutate(`/api/manage/admin/connector/${service}/app-credential`);
mutate(`/api/manage/admin/connector/${service}/service-account-key`);
};