mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
feat(frontend): add oauth2 credential support in new builder (#11107)
In this PR, I have added support of oAuth2 in new builder. https://github.com/user-attachments/assets/89472ebb-8ec2-467a-9824-79a80a71af8a ### Changes 🏗️ - Updated the FlowEditor to support OAuth2 credential selection. - Improved the UI for API key and OAuth2 modals, enhancing user experience. - Refactored credential field components for better modularity and maintainability. - Updated OpenAPI documentation to reflect changes in OAuth flow endpoints. ### Checklist 📋 - [x] I have clearly listed my changes in the PR description - [x] I have made a test plan - [x] I have tested my changes according to the test plan: - [x] Able to create OAuth credentials - [x] OAuth2 is correctly selected using the Credential Selector.
This commit is contained in:
@@ -64,7 +64,7 @@ class LoginResponse(BaseModel):
|
||||
state_token: str
|
||||
|
||||
|
||||
@router.get("/{provider}/login")
|
||||
@router.get("/{provider}/login", summary="Initiate OAuth flow")
|
||||
async def login(
|
||||
provider: Annotated[
|
||||
ProviderName, Path(title="The provider to initiate an OAuth flow for")
|
||||
@@ -102,7 +102,7 @@ class CredentialsMetaResponse(BaseModel):
|
||||
)
|
||||
|
||||
|
||||
@router.post("/{provider}/callback")
|
||||
@router.post("/{provider}/callback", summary="Exchange OAuth code for tokens")
|
||||
async def callback(
|
||||
provider: Annotated[
|
||||
ProviderName, Path(title="The target provider for this OAuth exchange")
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { FieldProps } from "@rjsf/utils";
|
||||
import { useCredentialField } from "./useCredentialField";
|
||||
import { KeyIcon, PlusIcon } from "@phosphor-icons/react";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { SelectCredential } from "./SelectCredential";
|
||||
import { Skeleton } from "@/components/__legacy__/ui/skeleton";
|
||||
import { BlockIOCredentialsSubSchema } from "@/lib/autogpt-server-api";
|
||||
import { APIKeyCredentialsModal } from "./models/APIKeyCredentialModal/APIKeyCredentialModal";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { OAuthCredentialModal } from "./models/OAuthCredentialModal/OAuthCredentialModal";
|
||||
|
||||
export const CredentialsField = (props: FieldProps) => {
|
||||
const { formData = {}, onChange, required: _required, schema } = props;
|
||||
@@ -16,8 +14,6 @@ export const CredentialsField = (props: FieldProps) => {
|
||||
isCredentialListLoading,
|
||||
supportsApiKey,
|
||||
supportsOAuth2,
|
||||
isAPIKeyModalOpen,
|
||||
setIsAPIKeyModalOpen,
|
||||
credentialsExists,
|
||||
} = useCredentialField({
|
||||
credentialSchema: schema as BlockIOCredentialsSubSchema,
|
||||
@@ -61,31 +57,13 @@ export const CredentialsField = (props: FieldProps) => {
|
||||
|
||||
<div>
|
||||
{supportsApiKey && (
|
||||
<>
|
||||
<APIKeyCredentialsModal
|
||||
schema={schema as BlockIOCredentialsSubSchema}
|
||||
open={isAPIKeyModalOpen}
|
||||
onClose={() => setIsAPIKeyModalOpen(false)}
|
||||
onSuccess={handleCredentialCreated}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
className="w-auto min-w-0"
|
||||
size="small"
|
||||
onClick={() => setIsAPIKeyModalOpen(true)}
|
||||
>
|
||||
<KeyIcon />
|
||||
<Text variant="body-medium" className="!text-white opacity-100">
|
||||
Add API key
|
||||
</Text>
|
||||
</Button>
|
||||
</>
|
||||
<APIKeyCredentialsModal
|
||||
schema={schema as BlockIOCredentialsSubSchema}
|
||||
onSuccess={handleCredentialCreated}
|
||||
/>
|
||||
)}
|
||||
{supportsOAuth2 && (
|
||||
<Button type="button" className="w-fit" size="small">
|
||||
<PlusIcon />
|
||||
Add OAuth2
|
||||
</Button>
|
||||
<OAuthCredentialModal provider={schema.credentials_provider[0]} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import React from "react";
|
||||
import { Select } from "@/components/atoms/Select/Select";
|
||||
import { CredentialsMetaResponse } from "@/app/api/__generated__/models/credentialsMetaResponse";
|
||||
import { ArrowSquareOutIcon, KeyIcon } from "@phosphor-icons/react";
|
||||
import {
|
||||
ArrowSquareOutIcon,
|
||||
KeyholeIcon,
|
||||
KeyIcon,
|
||||
} from "@phosphor-icons/react";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import Link from "next/link";
|
||||
import { providerIcons } from "./helpers";
|
||||
|
||||
type SelectCredentialProps = {
|
||||
credentials: CredentialsMetaResponse[];
|
||||
@@ -38,10 +43,22 @@ export const SelectCredential: React.FC<SelectCredentialProps> = ({
|
||||
? `${cred.provider} (${details.join(" - ")})`
|
||||
: cred.provider;
|
||||
|
||||
const Icon = providerIcons[cred.provider];
|
||||
const icon =
|
||||
cred.type === "oauth2" ? (
|
||||
Icon ? (
|
||||
<Icon />
|
||||
) : (
|
||||
<KeyholeIcon />
|
||||
)
|
||||
) : (
|
||||
<KeyIcon className="h-4 w-4" />
|
||||
);
|
||||
|
||||
return {
|
||||
value: cred.id,
|
||||
label,
|
||||
icon: <KeyIcon className="h-4 w-4" />,
|
||||
icon,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { CredentialsMetaResponse } from "@/app/api/__generated__/models/credentialsMetaResponse";
|
||||
// Need to replace these icons with phosphor icons
|
||||
import {
|
||||
FaDiscord,
|
||||
FaMedium,
|
||||
FaGithub,
|
||||
FaGoogle,
|
||||
FaHubspot,
|
||||
FaTwitter,
|
||||
} from "react-icons/fa";
|
||||
import { GoogleLogoIcon, KeyIcon, NotionLogoIcon } from "@phosphor-icons/react";
|
||||
GoogleLogoIcon,
|
||||
KeyholeIcon,
|
||||
NotionLogoIcon,
|
||||
DiscordLogoIcon,
|
||||
MediumLogoIcon,
|
||||
GithubLogoIcon,
|
||||
TwitterLogoIcon,
|
||||
Icon,
|
||||
} from "@phosphor-icons/react";
|
||||
|
||||
export const filterCredentialsByProvider = (
|
||||
credentials: CredentialsMetaResponse[] | undefined,
|
||||
@@ -56,46 +56,44 @@ export function isCredentialFieldSchema(schema: any): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
export const providerIcons: Partial<
|
||||
Record<string, React.FC<{ className?: string }>>
|
||||
> = {
|
||||
aiml_api: KeyIcon,
|
||||
anthropic: KeyIcon,
|
||||
apollo: KeyIcon,
|
||||
e2b: KeyIcon,
|
||||
github: FaGithub,
|
||||
export const providerIcons: Partial<Record<string, Icon>> = {
|
||||
aiml_api: KeyholeIcon,
|
||||
anthropic: KeyholeIcon,
|
||||
apollo: KeyholeIcon,
|
||||
e2b: KeyholeIcon,
|
||||
github: GithubLogoIcon,
|
||||
google: GoogleLogoIcon,
|
||||
groq: KeyIcon,
|
||||
http: KeyIcon,
|
||||
groq: KeyholeIcon,
|
||||
http: KeyholeIcon,
|
||||
notion: NotionLogoIcon,
|
||||
nvidia: KeyIcon,
|
||||
discord: FaDiscord,
|
||||
d_id: KeyIcon,
|
||||
google_maps: FaGoogle,
|
||||
jina: KeyIcon,
|
||||
ideogram: KeyIcon,
|
||||
linear: KeyIcon,
|
||||
medium: FaMedium,
|
||||
mem0: KeyIcon,
|
||||
ollama: KeyIcon,
|
||||
openai: KeyIcon,
|
||||
openweathermap: KeyIcon,
|
||||
open_router: KeyIcon,
|
||||
llama_api: KeyIcon,
|
||||
pinecone: KeyIcon,
|
||||
enrichlayer: KeyIcon,
|
||||
slant3d: KeyIcon,
|
||||
screenshotone: KeyIcon,
|
||||
smtp: KeyIcon,
|
||||
replicate: KeyIcon,
|
||||
reddit: KeyIcon,
|
||||
fal: KeyIcon,
|
||||
revid: KeyIcon,
|
||||
twitter: FaTwitter,
|
||||
unreal_speech: KeyIcon,
|
||||
exa: KeyIcon,
|
||||
hubspot: FaHubspot,
|
||||
smartlead: KeyIcon,
|
||||
todoist: KeyIcon,
|
||||
zerobounce: KeyIcon,
|
||||
nvidia: KeyholeIcon,
|
||||
discord: DiscordLogoIcon,
|
||||
d_id: KeyholeIcon,
|
||||
google_maps: GoogleLogoIcon,
|
||||
jina: KeyholeIcon,
|
||||
ideogram: KeyholeIcon,
|
||||
linear: KeyholeIcon,
|
||||
medium: MediumLogoIcon,
|
||||
mem0: KeyholeIcon,
|
||||
ollama: KeyholeIcon,
|
||||
openai: KeyholeIcon,
|
||||
openweathermap: KeyholeIcon,
|
||||
open_router: KeyholeIcon,
|
||||
llama_api: KeyholeIcon,
|
||||
pinecone: KeyholeIcon,
|
||||
enrichlayer: KeyholeIcon,
|
||||
slant3d: KeyholeIcon,
|
||||
screenshotone: KeyholeIcon,
|
||||
smtp: KeyholeIcon,
|
||||
replicate: KeyholeIcon,
|
||||
reddit: KeyholeIcon,
|
||||
fal: KeyholeIcon,
|
||||
revid: KeyholeIcon,
|
||||
twitter: TwitterLogoIcon,
|
||||
unreal_speech: KeyholeIcon,
|
||||
exa: KeyholeIcon,
|
||||
hubspot: KeyholeIcon,
|
||||
smartlead: KeyholeIcon,
|
||||
todoist: KeyholeIcon,
|
||||
zerobounce: KeyholeIcon,
|
||||
};
|
||||
|
||||
@@ -9,111 +9,127 @@ import {
|
||||
import { BlockIOCredentialsSubSchema } from "@/lib/autogpt-server-api/types"; // we need to find a way to replace it with autogenerated types
|
||||
import { useAPIKeyCredentialsModal } from "./useAPIKeyCredentialsModal";
|
||||
import { toDisplayName } from "../../helpers";
|
||||
import { KeyIcon } from "@phosphor-icons/react";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
|
||||
type Props = {
|
||||
schema: BlockIOCredentialsSubSchema;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
|
||||
onSuccess: (credentialId: string) => void;
|
||||
};
|
||||
|
||||
export function APIKeyCredentialsModal({
|
||||
schema,
|
||||
open,
|
||||
onClose,
|
||||
onSuccess,
|
||||
}: Props) {
|
||||
const { form, isLoading, schemaDescription, onSubmit, provider } =
|
||||
useAPIKeyCredentialsModal({ schema, onClose, onSuccess });
|
||||
export function APIKeyCredentialsModal({ schema, onSuccess }: Props) {
|
||||
const {
|
||||
form,
|
||||
isLoading,
|
||||
schemaDescription,
|
||||
onSubmit,
|
||||
provider,
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
} = useAPIKeyCredentialsModal({ schema, onSuccess });
|
||||
|
||||
if (isLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
title={`Add new API key for ${toDisplayName(provider) ?? ""}`}
|
||||
controlled={{
|
||||
isOpen: open,
|
||||
set: (isOpen) => {
|
||||
if (!isOpen) onClose();
|
||||
},
|
||||
}}
|
||||
onClose={onClose}
|
||||
styling={{
|
||||
maxWidth: "25rem",
|
||||
}}
|
||||
>
|
||||
<Dialog.Content>
|
||||
{schemaDescription && (
|
||||
<p className="mb-4 text-sm text-zinc-600">{schemaDescription}</p>
|
||||
)}
|
||||
<>
|
||||
<Dialog
|
||||
title={`Add new API key for ${toDisplayName(provider) ?? ""}`}
|
||||
controlled={{
|
||||
isOpen: isOpen,
|
||||
set: (isOpen) => {
|
||||
if (!isOpen) setIsOpen(false);
|
||||
},
|
||||
}}
|
||||
onClose={() => setIsOpen(false)}
|
||||
styling={{
|
||||
maxWidth: "25rem",
|
||||
}}
|
||||
>
|
||||
<Dialog.Content>
|
||||
{schemaDescription && (
|
||||
<p className="mb-4 text-sm text-zinc-600">{schemaDescription}</p>
|
||||
)}
|
||||
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="apiKey"
|
||||
render={({ field }) => (
|
||||
<>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="apiKey"
|
||||
render={({ field }) => (
|
||||
<>
|
||||
<Input
|
||||
id="apiKey"
|
||||
label="API Key"
|
||||
type="password"
|
||||
placeholder="Enter API key..."
|
||||
size="small"
|
||||
hint={
|
||||
schema.credentials_scopes ? (
|
||||
<FormDescription>
|
||||
Required scope(s) for this block:{" "}
|
||||
{schema.credentials_scopes?.map((s, i, a) => (
|
||||
<span key={i}>
|
||||
<code className="text-xs font-bold">{s}</code>
|
||||
{i < a.length - 1 && ", "}
|
||||
</span>
|
||||
))}
|
||||
</FormDescription>
|
||||
) : null
|
||||
}
|
||||
{...field}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="title"
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
id="apiKey"
|
||||
label="API Key"
|
||||
type="password"
|
||||
placeholder="Enter API key..."
|
||||
id="title"
|
||||
label="Name"
|
||||
type="text"
|
||||
placeholder="Enter a name for this API key..."
|
||||
size="small"
|
||||
hint={
|
||||
schema.credentials_scopes ? (
|
||||
<FormDescription>
|
||||
Required scope(s) for this block:{" "}
|
||||
{schema.credentials_scopes?.map((s, i, a) => (
|
||||
<span key={i}>
|
||||
<code className="text-xs font-bold">{s}</code>
|
||||
{i < a.length - 1 && ", "}
|
||||
</span>
|
||||
))}
|
||||
</FormDescription>
|
||||
) : null
|
||||
}
|
||||
{...field}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="title"
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
id="title"
|
||||
label="Name"
|
||||
type="text"
|
||||
placeholder="Enter a name for this API key..."
|
||||
size="small"
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="expiresAt"
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
id="expiresAt"
|
||||
label="Expiration Date"
|
||||
type="datetime-local"
|
||||
placeholder="Select expiration date..."
|
||||
size="small"
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit" size="small" className="min-w-68">
|
||||
Save & use this API key
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</Dialog.Content>
|
||||
</Dialog>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="expiresAt"
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
id="expiresAt"
|
||||
label="Expiration Date"
|
||||
type="datetime-local"
|
||||
placeholder="Select expiration date..."
|
||||
size="small"
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit" size="small" className="min-w-68">
|
||||
Save & use this API key
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</Dialog.Content>
|
||||
</Dialog>
|
||||
<Button
|
||||
type="button"
|
||||
className="w-auto min-w-0"
|
||||
size="small"
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
<KeyIcon />
|
||||
<Text variant="body-medium" className="!text-white opacity-100">
|
||||
Add API key
|
||||
</Text>
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useToast } from "@/components/molecules/Toast/use-toast";
|
||||
import { APIKeyCredentials } from "@/app/api/__generated__/models/aPIKeyCredentials";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { PostV1CreateCredentials201 } from "@/app/api/__generated__/models/postV1CreateCredentials201";
|
||||
import { useState } from "react";
|
||||
|
||||
export type APIKeyFormValues = {
|
||||
apiKey: string;
|
||||
@@ -19,13 +20,11 @@ export type APIKeyFormValues = {
|
||||
|
||||
type useAPIKeyCredentialsModalType = {
|
||||
schema: BlockIOCredentialsSubSchema;
|
||||
onClose: () => void;
|
||||
onSuccess: (credentialId: string) => void;
|
||||
};
|
||||
|
||||
export function useAPIKeyCredentialsModal({
|
||||
schema,
|
||||
onClose,
|
||||
onSuccess,
|
||||
}: useAPIKeyCredentialsModalType): {
|
||||
form: UseFormReturn<APIKeyFormValues>;
|
||||
@@ -33,8 +32,11 @@ export function useAPIKeyCredentialsModal({
|
||||
provider: string;
|
||||
schemaDescription?: string;
|
||||
onSubmit: (values: APIKeyFormValues) => Promise<void>;
|
||||
isOpen: boolean;
|
||||
setIsOpen: (isOpen: boolean) => void;
|
||||
} {
|
||||
const { toast } = useToast();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { mutateAsync: createCredentials, isPending: isCreatingCredentials } =
|
||||
@@ -43,8 +45,8 @@ export function useAPIKeyCredentialsModal({
|
||||
onSuccess: async (response) => {
|
||||
const credentialId = (response.data as PostV1CreateCredentials201)
|
||||
?.id;
|
||||
onClose();
|
||||
form.reset();
|
||||
setIsOpen(false);
|
||||
toast({
|
||||
title: "Success",
|
||||
description: "Credentials created successfully",
|
||||
@@ -107,5 +109,7 @@ export function useAPIKeyCredentialsModal({
|
||||
provider: schema.credentials_provider[0],
|
||||
schemaDescription: schema.description,
|
||||
onSubmit,
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { Dialog } from "@/components/molecules/Dialog/Dialog";
|
||||
import { providerIcons, toDisplayName } from "../../helpers";
|
||||
import { useOAuthCredentialModal } from "./useOAuthCredentialModal";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
|
||||
type OAuthCredentialModalProps = {
|
||||
provider: string;
|
||||
};
|
||||
|
||||
export const OAuthCredentialModal = ({
|
||||
provider,
|
||||
}: OAuthCredentialModalProps) => {
|
||||
const Icon = providerIcons[provider];
|
||||
const { handleOAuthLogin, loading, error, onClose, open, setOpen } =
|
||||
useOAuthCredentialModal({
|
||||
provider,
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<Dialog
|
||||
title={`Waiting on ${toDisplayName(provider)} sign-in process...`}
|
||||
controlled={{
|
||||
isOpen: open,
|
||||
set: (isOpen) => {
|
||||
if (!isOpen) setOpen(false);
|
||||
},
|
||||
}}
|
||||
onClose={onClose}
|
||||
>
|
||||
<Dialog.Content>
|
||||
<p className="text-sm text-zinc-600">
|
||||
Complete the sign-in process in the pop-up window.
|
||||
<br />
|
||||
Closing this dialog will cancel the sign-in process.
|
||||
</p>
|
||||
</Dialog.Content>
|
||||
</Dialog>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
className="w-fit"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
handleOAuthLogin();
|
||||
}}
|
||||
disabled={loading}
|
||||
>
|
||||
{Icon && <Icon className="size-4" />}
|
||||
Add OAuth2
|
||||
</Button>
|
||||
{error && (
|
||||
<div className="mt-2 flex w-fit items-center rounded-full bg-red-50 p-1 px-3 ring-1 ring-red-600">
|
||||
<Text variant="small" className="!text-red-600">
|
||||
{error as string}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,198 @@
|
||||
import {
|
||||
getGetV1ListCredentialsQueryKey,
|
||||
useGetV1InitiateOauthFlow,
|
||||
usePostV1ExchangeOauthCodeForTokens,
|
||||
} from "@/app/api/__generated__/endpoints/integrations/integrations";
|
||||
import { LoginResponse } from "@/app/api/__generated__/models/loginResponse";
|
||||
import { useToast } from "@/components/molecules/Toast/use-toast";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
|
||||
type useOAuthCredentialModalProps = {
|
||||
provider: string;
|
||||
scopes?: string[];
|
||||
};
|
||||
|
||||
export type OAuthPopupResultMessage = { message_type: "oauth_popup_result" } & (
|
||||
| {
|
||||
success: true;
|
||||
code: string;
|
||||
state: string;
|
||||
}
|
||||
| {
|
||||
success: false;
|
||||
message: string;
|
||||
}
|
||||
);
|
||||
|
||||
export const useOAuthCredentialModal = ({
|
||||
provider,
|
||||
scopes,
|
||||
}: useOAuthCredentialModalProps) => {
|
||||
const { toast } = useToast();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [oAuthPopupController, setOAuthPopupController] =
|
||||
useState<AbortController | null>(null);
|
||||
const [oAuthError, setOAuthError] = useState<string | null>(null);
|
||||
const [isOAuth2FlowInProgress, setOAuth2FlowInProgress] = useState(false);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const {
|
||||
refetch: initiateOauthFlow,
|
||||
isRefetching: isInitiatingOauthFlow,
|
||||
isRefetchError: initiatingOauthFlowError,
|
||||
} = useGetV1InitiateOauthFlow(
|
||||
provider,
|
||||
{
|
||||
scopes: scopes?.join(","),
|
||||
},
|
||||
{
|
||||
query: {
|
||||
enabled: false,
|
||||
select: (res) => {
|
||||
return res.data as LoginResponse;
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const {
|
||||
mutateAsync: oAuthCallback,
|
||||
isPending: isOAuthCallbackPending,
|
||||
error: oAuthCallbackError,
|
||||
} = usePostV1ExchangeOauthCodeForTokens({
|
||||
mutation: {
|
||||
onSuccess: (data) => {
|
||||
console.log("OAuth callback successful", data);
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: getGetV1ListCredentialsQueryKey(),
|
||||
});
|
||||
setOpen(false);
|
||||
toast({
|
||||
title: "Success",
|
||||
description: "Credential added successfully",
|
||||
variant: "default",
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const handleOAuthLogin = async () => {
|
||||
const { data } = await initiateOauthFlow();
|
||||
if (!data || !data.login_url || !data.state_token) {
|
||||
toast({
|
||||
title: "Failed to initiate OAuth flow",
|
||||
variant: "destructive",
|
||||
});
|
||||
setOAuthError(
|
||||
data && typeof data === "object" && "detail" in data
|
||||
? (data.detail as string)
|
||||
: "Failed to initiate OAuth flow",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setOpen(true);
|
||||
setOAuth2FlowInProgress(true);
|
||||
|
||||
const { login_url, state_token } = data;
|
||||
|
||||
const popup = window.open(login_url, "_blank", "popup=true");
|
||||
|
||||
if (!popup) {
|
||||
throw new Error(
|
||||
"Failed to open popup window. Please allow popups for this site.",
|
||||
);
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
setOAuthPopupController(controller);
|
||||
|
||||
controller.signal.onabort = () => {
|
||||
console.debug("OAuth flow aborted");
|
||||
popup.close();
|
||||
};
|
||||
|
||||
const handleMessage = async (e: MessageEvent<OAuthPopupResultMessage>) => {
|
||||
console.log("inside handleMessage");
|
||||
console.debug("Message received:", e.data);
|
||||
if (
|
||||
typeof e.data != "object" ||
|
||||
!("message_type" in e.data) ||
|
||||
e.data.message_type !== "oauth_popup_result"
|
||||
) {
|
||||
console.debug("Ignoring irrelevant message");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!e.data.success) {
|
||||
console.error("OAuth flow failed:", e.data.message);
|
||||
setOAuthError(`OAuth flow failed: ${e.data.message}`);
|
||||
setOAuth2FlowInProgress(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.data.state !== state_token) {
|
||||
console.error("Invalid state token received");
|
||||
setOAuthError("Invalid state token received");
|
||||
setOAuth2FlowInProgress(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.debug("Processing OAuth callback");
|
||||
await oAuthCallback({
|
||||
provider,
|
||||
data: {
|
||||
code: e.data.code,
|
||||
state_token: e.data.state,
|
||||
},
|
||||
});
|
||||
|
||||
console.debug("OAuth callback processed successfully");
|
||||
} catch (error) {
|
||||
console.error("Error in OAuth callback:", error);
|
||||
setOAuthError(
|
||||
`Error in OAuth callback: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
);
|
||||
} finally {
|
||||
console.debug("Finalizing OAuth flow");
|
||||
setOAuth2FlowInProgress(false);
|
||||
controller.abort("success");
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("message", handleMessage, {
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
setTimeout(
|
||||
() => {
|
||||
console.debug("OAuth flow timed out");
|
||||
controller.abort("timeout");
|
||||
setOAuth2FlowInProgress(false);
|
||||
setOAuthError("OAuth flow timed out");
|
||||
},
|
||||
5 * 60 * 1000,
|
||||
);
|
||||
};
|
||||
|
||||
const onClose = () => {
|
||||
oAuthPopupController?.abort("canceled");
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return {
|
||||
handleOAuthLogin,
|
||||
loading:
|
||||
isOAuth2FlowInProgress || isOAuthCallbackPending || isInitiatingOauthFlow,
|
||||
error: oAuthError || initiatingOauthFlowError || oAuthCallbackError,
|
||||
onClose,
|
||||
open,
|
||||
setOpen,
|
||||
};
|
||||
};
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useGetV1ListCredentials } from "@/app/api/__generated__/endpoints/integrations/integrations";
|
||||
import { CredentialsMetaResponse } from "@/app/api/__generated__/models/credentialsMetaResponse";
|
||||
import { BlockIOCredentialsSubSchema } from "@/lib/autogpt-server-api";
|
||||
import { useState } from "react";
|
||||
import { filterCredentialsByProvider } from "./helpers";
|
||||
|
||||
export const useCredentialField = ({
|
||||
@@ -9,8 +8,6 @@ export const useCredentialField = ({
|
||||
}: {
|
||||
credentialSchema: BlockIOCredentialsSubSchema; // Here we are using manual typing, we need to fix it with automatic one
|
||||
}) => {
|
||||
const [isAPIKeyModalOpen, setIsAPIKeyModalOpen] = useState(false);
|
||||
|
||||
// Fetch all the credentials from the backend
|
||||
// We will save it in cache for 10 min, if user edits the credential, we will invalidate the cache
|
||||
// Whenever user adds a block, we filter the credentials list and check if this block's provider is in the list
|
||||
@@ -36,8 +33,6 @@ export const useCredentialField = ({
|
||||
isCredentialListLoading,
|
||||
supportsApiKey,
|
||||
supportsOAuth2,
|
||||
isAPIKeyModalOpen,
|
||||
setIsAPIKeyModalOpen,
|
||||
credentialsExists,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
"/api/integrations/{provider}/login": {
|
||||
"get": {
|
||||
"tags": ["v1", "integrations"],
|
||||
"summary": "Login",
|
||||
"operationId": "getV1Login",
|
||||
"summary": "Initiate OAuth flow",
|
||||
"operationId": "getV1Initiate oauth flow",
|
||||
"security": [{ "HTTPBearerJWT": [] }],
|
||||
"parameters": [
|
||||
{
|
||||
@@ -60,8 +60,8 @@
|
||||
"/api/integrations/{provider}/callback": {
|
||||
"post": {
|
||||
"tags": ["v1", "integrations"],
|
||||
"summary": "Callback",
|
||||
"operationId": "postV1Callback",
|
||||
"summary": "Exchange OAuth code for tokens",
|
||||
"operationId": "postV1Exchange oauth code for tokens",
|
||||
"security": [{ "HTTPBearerJWT": [] }],
|
||||
"parameters": [
|
||||
{
|
||||
@@ -79,7 +79,9 @@
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/Body_postV1Callback" }
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Body_postV1Exchange_oauth_code_for_tokens"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -5062,7 +5064,7 @@
|
||||
"required": ["blocks", "pagination"],
|
||||
"title": "BlockResponse"
|
||||
},
|
||||
"Body_postV1Callback": {
|
||||
"Body_postV1Exchange_oauth_code_for_tokens": {
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "string",
|
||||
@@ -5072,7 +5074,7 @@
|
||||
},
|
||||
"type": "object",
|
||||
"required": ["code", "state_token"],
|
||||
"title": "Body_postV1Callback"
|
||||
"title": "Body_postV1Exchange oauth code for tokens"
|
||||
},
|
||||
"Body_postV1Execute_graph_agent": {
|
||||
"properties": {
|
||||
|
||||
Reference in New Issue
Block a user