Compare commits

..

8 Commits

Author SHA1 Message Date
pablonyx
ddaaaeeb40 k 2025-02-27 20:28:49 -08:00
pablonyx
4e31bc19dc k 2025-02-27 17:02:11 -08:00
pablonyx
1c0c80116e fix function 2025-02-27 17:00:05 -08:00
pablonyx
27891ad34d k 2025-02-27 16:51:06 -08:00
pablonyx
511341fd7c k 2025-02-27 16:49:23 -08:00
pablonyx
88d4e7defa fix handling 2025-02-27 16:14:02 -08:00
pablonyx
a7ba0da8cc Lowercase multi tenant email mapping (#4141) 2025-02-27 15:33:40 -08:00
Richard Kuo (Danswer)
aaced6d551 scan images 2025-02-27 15:25:29 -08:00
17 changed files with 836 additions and 404 deletions

View File

@@ -62,19 +62,81 @@ jobs:
# be careful enabling the sarif and upload as it may spam the security tab
# with a huge amount of items. Work out the issues before enabling upload.
- name: Run Trivy vulnerability scanner in repo mode
if: always()
uses: aquasecurity/trivy-action@0.29.0
with:
scan-type: fs
scan-ref: .
scanners: license
format: table
severity: HIGH,CRITICAL
# format: sarif
# output: trivy-results.sarif
# - name: Upload Trivy scan results to GitHub Security tab
# uses: github/codeql-action/upload-sarif@v3
# - name: Run Trivy vulnerability scanner in repo mode
# if: always()
# uses: aquasecurity/trivy-action@0.29.0
# with:
# sarif_file: trivy-results.sarif
# scan-type: fs
# scan-ref: .
# scanners: license
# format: table
# severity: HIGH,CRITICAL
# # format: sarif
# # output: trivy-results.sarif
#
# # - name: Upload Trivy scan results to GitHub Security tab
# # uses: github/codeql-action/upload-sarif@v3
# # with:
# # sarif_file: trivy-results.sarif
scan-trivy:
# See https://runs-on.com/runners/linux/
runs-on: [runs-on,runner=2cpu-linux-x64,"run-id=${{ github.run_id }}"]
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
# Backend
- name: Pull backend docker image
run: docker pull onyxdotapp/onyx-backend:latest
- name: Run Trivy vulnerability scanner on backend
uses: aquasecurity/trivy-action@0.29.0
env:
TRIVY_DB_REPOSITORY: 'public.ecr.aws/aquasecurity/trivy-db:2'
TRIVY_JAVA_DB_REPOSITORY: 'public.ecr.aws/aquasecurity/trivy-java-db:1'
with:
image-ref: onyxdotapp/onyx-backend:latest
scanners: license
severity: HIGH,CRITICAL
vuln-type: library
exit-code: 0 # Set to 1 if we want a failed scan to fail the workflow
# Web server
- name: Pull web server docker image
run: docker pull onyxdotapp/onyx-web-server:latest
- name: Run Trivy vulnerability scanner on web server
uses: aquasecurity/trivy-action@0.29.0
env:
TRIVY_DB_REPOSITORY: 'public.ecr.aws/aquasecurity/trivy-db:2'
TRIVY_JAVA_DB_REPOSITORY: 'public.ecr.aws/aquasecurity/trivy-java-db:1'
with:
image-ref: onyxdotapp/onyx-web-server:latest
scanners: license
severity: HIGH,CRITICAL
vuln-type: library
exit-code: 0
# Model server
- name: Pull model server docker image
run: docker pull onyxdotapp/onyx-model-server:latest
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@0.29.0
env:
TRIVY_DB_REPOSITORY: 'public.ecr.aws/aquasecurity/trivy-db:2'
TRIVY_JAVA_DB_REPOSITORY: 'public.ecr.aws/aquasecurity/trivy-java-db:1'
with:
image-ref: onyxdotapp/onyx-model-server:latest
scanners: license
severity: HIGH,CRITICAL
vuln-type: library
exit-code: 0

View File

@@ -0,0 +1,36 @@
"""force lowercase all users
Revision ID: f11b408e39d3
Revises: 3bd4c84fe72f
Create Date: 2025-02-26 17:04:55.683500
"""
# revision identifiers, used by Alembic.
revision = "f11b408e39d3"
down_revision = "3bd4c84fe72f"
branch_labels = None
depends_on = None
def upgrade() -> None:
# 1) Convert all existing user emails to lowercase
from alembic import op
op.execute(
"""
UPDATE "user"
SET email = LOWER(email)
"""
)
# 2) Add a check constraint to ensure emails are always lowercase
op.create_check_constraint("ensure_lowercase_email", "user", "email = LOWER(email)")
def downgrade() -> None:
# Drop the check constraint
from alembic import op
op.drop_constraint("ensure_lowercase_email", "user", type_="check")

View File

@@ -0,0 +1,42 @@
"""lowercase multi-tenant user auth
Revision ID: 34e3630c7f32
Revises: a4f6ee863c47
Create Date: 2025-02-26 15:03:01.211894
"""
from alembic import op
# revision identifiers, used by Alembic.
revision = "34e3630c7f32"
down_revision = "a4f6ee863c47"
branch_labels = None
depends_on = None
def upgrade() -> None:
# 1) Convert all existing rows to lowercase
op.execute(
"""
UPDATE user_tenant_mapping
SET email = LOWER(email)
"""
)
# 2) Add a check constraint so that emails cannot be written in uppercase
op.create_check_constraint(
"ensure_lowercase_email",
"user_tenant_mapping",
"email = LOWER(email)",
schema="public",
)
def downgrade() -> None:
# Drop the check constraint
op.drop_constraint(
"ensure_lowercase_email",
"user_tenant_mapping",
schema="public",
type_="check",
)

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

@@ -7,6 +7,7 @@ from typing import Optional
from uuid import uuid4
from pydantic import BaseModel
from sqlalchemy.orm import validates
from typing_extensions import TypedDict # noreorder
from uuid import UUID
@@ -206,6 +207,10 @@ class User(SQLAlchemyBaseUserTableUUID, Base):
primaryjoin="User.id == foreign(ConnectorCredentialPair.creator_id)",
)
@validates("email")
def validate_email(self, key: str, value: str) -> str:
return value.lower() if value else value
@property
def password_configured(self) -> bool:
"""
@@ -2270,6 +2275,10 @@ class UserTenantMapping(Base):
email: Mapped[str] = mapped_column(String, nullable=False, primary_key=True)
tenant_id: Mapped[str] = mapped_column(String, nullable=False)
@validates("email")
def validate_email(self, key: str, value: str) -> str:
return value.lower() if value else value
# This is a mapping from tenant IDs to anonymous user paths
class TenantAnonymousUserPath(Base):

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

@@ -343,8 +343,7 @@ def list_bot_configs(
]
MAX_SLACK_PAGES = 5
SLACK_API_CHANNELS_PER_PAGE = 100
MAX_CHANNELS = 200
@router.get(
@@ -356,8 +355,8 @@ def get_all_channels_from_slack_api(
_: User | None = Depends(current_admin_user),
) -> list[SlackChannel]:
"""
Fetches channels the bot is a member of from the Slack API.
Handles pagination with a limit to avoid excessive API calls.
Fetches all channels from the Slack API.
If the workspace has 200 or more channels, we raise an error.
"""
tokens = fetch_slack_bot_tokens(db_session, bot_id)
if not tokens or "bot_token" not in tokens:
@@ -366,60 +365,28 @@ def get_all_channels_from_slack_api(
)
client = WebClient(token=tokens["bot_token"])
all_channels = []
next_cursor = None
current_page = 0
try:
# Use users_conversations with limited pagination
while current_page < MAX_SLACK_PAGES:
current_page += 1
# Make API call with cursor if we have one
if next_cursor:
response = client.users_conversations(
types="public_channel,private_channel",
exclude_archived=True,
cursor=next_cursor,
limit=SLACK_API_CHANNELS_PER_PAGE,
)
else:
response = client.users_conversations(
types="public_channel,private_channel",
exclude_archived=True,
limit=SLACK_API_CHANNELS_PER_PAGE,
)
# Add channels to our list
if "channels" in response and response["channels"]:
all_channels.extend(response["channels"])
# Check if we need to paginate
if (
"response_metadata" in response
and "next_cursor" in response["response_metadata"]
):
next_cursor = response["response_metadata"]["next_cursor"]
if next_cursor:
if current_page == MAX_SLACK_PAGES:
raise HTTPException(
status_code=400,
detail="Workspace has too many channels to paginate over in this call.",
)
continue
# If we get here, no more pages
break
response = client.conversations_list(
types="public_channel,private_channel",
exclude_archived=True,
limit=MAX_CHANNELS,
)
channels = [
SlackChannel(id=channel["id"], name=channel["name"])
for channel in all_channels
for channel in response["channels"]
]
if len(channels) == MAX_CHANNELS:
raise HTTPException(
status_code=400,
detail=f"Workspace has {MAX_CHANNELS} or more channels.",
)
return channels
except SlackApiError as e:
# Handle rate limiting or other API errors
raise HTTPException(
status_code=500,
detail=f"Error fetching channels from Slack API: {str(e)}",

View File

@@ -199,17 +199,17 @@ export function SlackChannelConfigFormFields({
<Badge variant="agent" className="bg-blue-100 text-blue-800">
Default Configuration
</Badge>
<p className="mt-2 text-sm text-neutral-600">
<p className="mt-2 text-sm text-gray-600">
This default configuration will apply across all Slack channels
the bot is added to in the Slack workspace, as well as direct
messages (DMs), unless disabled.
</p>
<div className="mt-4 p-4 bg-neutral-100 rounded-md border border-neutral-300">
<div className="mt-4 p-4 bg-gray-100 rounded-md border border-gray-300">
<CheckFormField
name="disabled"
label="Disable Default Configuration"
/>
<p className="mt-2 text-sm text-neutral-600 italic">
<p className="mt-2 text-sm text-gray-600 italic">
Warning: Disabling the default configuration means the bot
won&apos;t respond in Slack channels or DMs unless explicitly
configured for them.
@@ -238,28 +238,20 @@ export function SlackChannelConfigFormFields({
/>
</div>
) : (
<>
<Field name="channel_name">
{({ field, form }: { field: any; form: any }) => (
<SearchMultiSelectDropdown
options={channelOptions || []}
onSelect={(selected) => {
form.setFieldValue("channel_name", selected.name);
}}
initialSearchTerm={field.value}
onSearchTermChange={(term) => {
form.setFieldValue("channel_name", term);
}}
/>
)}
</Field>
<p className="mt-2 text-sm dark:text-neutral-400 text-neutral-600">
Note: This list shows public and private channels where the
bot is a member (up to 500 channels). If you don&apos;t see a
channel, make sure the bot is added to that channel in Slack
first, or type the channel name manually.
</p>
</>
<Field name="channel_name">
{({ field, form }: { field: any; form: any }) => (
<SearchMultiSelectDropdown
options={channelOptions || []}
onSelect={(selected) => {
form.setFieldValue("channel_name", selected.name);
}}
initialSearchTerm={field.value}
onSearchTermChange={(term) => {
form.setFieldValue("channel_name", term);
}}
/>
)}
</Field>
)}
</>
)}

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

@@ -1,16 +0,0 @@
export default function AuthErrorLayout({
children,
}: {
children: React.ReactNode;
}) {
// Log error to console for debugging
console.error(
"Authentication error page was accessed - this should not happen in normal flow"
);
// In a production environment, you might want to send this to your error tracking service
// For example, if using a service like Sentry:
// captureException(new Error("Authentication error page was accessed unexpectedly"));
return <>{children}</>;
}

View File

@@ -4,7 +4,6 @@ import AuthFlowContainer from "@/components/auth/AuthFlowContainer";
import { Button } from "@/components/ui/button";
import Link from "next/link";
import { FiLogIn } from "react-icons/fi";
import { NEXT_PUBLIC_CLOUD_ENABLED } from "@/lib/constants";
const Page = () => {
return (
@@ -16,21 +15,19 @@ const Page = () => {
<p className="text-text-700 text-center">
We encountered an issue while attempting to log you in.
</p>
<div className="bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-800 rounded-lg p-4 shadow-sm">
<h3 className="text-red-800 dark:text-red-400 font-semibold mb-2">
Possible Issues:
</h3>
<div className="bg-red-50 border border-red-200 rounded-lg p-4 shadow-sm">
<h3 className="text-red-800 font-semibold mb-2">Possible Issues:</h3>
<ul className="space-y-2">
<li className="flex items-center text-red-700 dark:text-red-400">
<div className="w-2 h-2 bg-red-500 dark:bg-red-400 rounded-full mr-2"></div>
<li className="flex items-center text-red-700">
<div className="w-2 h-2 bg-red-500 rounded-full mr-2"></div>
Incorrect or expired login credentials
</li>
<li className="flex items-center text-red-700 dark:text-red-400">
<div className="w-2 h-2 bg-red-500 dark:bg-red-400 rounded-full mr-2"></div>
<li className="flex items-center text-red-700">
<div className="w-2 h-2 bg-red-500 rounded-full mr-2"></div>
Temporary authentication system disruption
</li>
<li className="flex items-center text-red-700 dark:text-red-400">
<div className="w-2 h-2 bg-red-500 dark:bg-red-400 rounded-full mr-2"></div>
<li className="flex items-center text-red-700">
<div className="w-2 h-2 bg-red-500 rounded-full mr-2"></div>
Account access restrictions or permissions
</li>
</ul>
@@ -44,12 +41,6 @@ const Page = () => {
<p className="text-sm text-text-500 text-center">
We recommend trying again. If you continue to experience problems,
please reach out to your system administrator for assistance.
{NEXT_PUBLIC_CLOUD_ENABLED && (
<span className="block mt-1 text-blue-600">
A member of our team has been automatically notified about this
issue.
</span>
)}
</p>
</div>
</AuthFlowContainer>

View File

@@ -36,11 +36,14 @@ export function EmailPasswordForm({
{popup}
<Formik
initialValues={{
email: defaultEmail || "",
email: defaultEmail ? defaultEmail.toLowerCase() : "",
password: "",
}}
validationSchema={Yup.object().shape({
email: Yup.string().email().required(),
email: Yup.string()
.email()
.required()
.transform((value) => value.toLowerCase()),
password: Yup.string().required(),
})}
onSubmit={async (values) => {

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`);
};