feat(platform): Add username+password credentials type; fix email and reddit blocks (#9113)
<!-- Clearly explain the need for these changes: --> Update and adds a basic credential field for use in integrations like reddit ### Changes 🏗️ <!-- Concisely describe all of the changes made in this pull request: --> - Reddit - Drops the Username and Password for reddit from the .env - Updates Reddit block with modern provider and credential system - moves clientid and secret to reading from `Settings().secrets` rather than input on the block - moves user agent to `Settings().config` - SMTP - update the block to support user password and modern credentials - Add `UserPasswordCredentials` - Default API key expiry to None explicitly to help type cohesion - add `UserPasswordCredentials` with a weird form of `bearer` which we ideally remove because `basic` is a more appropriate name. This is dependent on `Webhook _base` allowing a subset of `Credentials` - Update `Credentials` and `CredentialsType` - Fix various `OAuth2Credentials | APIKeyCredentials` -> `Credentials` mismatches between base and derived classes - Replace `router/@post(create_api_key_credentials)` with `create_credentials` which now takes a credential and is discriminated by `type` provided by the credential - UI/Frontend - Updated various pages to have saved credential types, icons, and text for User Pass Credentials - Update credential input to have an input/modals/selects for user/pass combos - Update the types to support having user/pass credentials too (we should make this more centralized) - Update Credential Providres to support user_password - Update `client.ts` to support the new streamlined credential creation method and endpoint - DX - Sort the provider names **again** TODO: - [x] Reactivate Conditionally Disabling Reddit ~~- [ ] Look into moving Webhooks base to allow subset of `Credentials` rather than requiring all webhooks to support the input of all valid `Credentials` types~~ Out of scope - [x] Figure out the `singleCredential` calculator in `credentials-input.tsx` so that it also respects User Pass credentials and isn't a logic mess ### Checklist 📋 #### For code changes: - [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: <!-- Put your test plan here: --> - [x] Test with agents --------- Co-authored-by: Zamil Majdy <zamil.majdy@agpt.co>pull/9138/head
parent
97ecaf5639
commit
ac8a466cda
|
@ -91,10 +91,12 @@ GROQ_API_KEY=
|
|||
OPEN_ROUTER_API_KEY=
|
||||
|
||||
# Reddit
|
||||
# Go to https://www.reddit.com/prefs/apps and create a new app
|
||||
# Choose "script" for the type
|
||||
# Fill in the redirect uri as <your_frontend_url>/auth/integrations/oauth_callback, e.g. http://localhost:3000/auth/integrations/oauth_callback
|
||||
REDDIT_CLIENT_ID=
|
||||
REDDIT_CLIENT_SECRET=
|
||||
REDDIT_USERNAME=
|
||||
REDDIT_PASSWORD=
|
||||
REDDIT_USER_AGENT="AutoGPT:1.0 (by /u/autogpt)"
|
||||
|
||||
# Discord
|
||||
DISCORD_BOT_TOKEN=
|
||||
|
|
|
@ -1,22 +1,53 @@
|
|||
import smtplib
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from pydantic import BaseModel, ConfigDict, SecretStr
|
||||
|
||||
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
|
||||
from backend.data.model import BlockSecret, SchemaField, SecretField
|
||||
from backend.data.model import (
|
||||
CredentialsField,
|
||||
CredentialsMetaInput,
|
||||
SchemaField,
|
||||
UserPasswordCredentials,
|
||||
)
|
||||
from backend.integrations.providers import ProviderName
|
||||
|
||||
TEST_CREDENTIALS = UserPasswordCredentials(
|
||||
id="01234567-89ab-cdef-0123-456789abcdef",
|
||||
provider="smtp",
|
||||
username=SecretStr("mock-smtp-username"),
|
||||
password=SecretStr("mock-smtp-password"),
|
||||
title="Mock SMTP credentials",
|
||||
)
|
||||
|
||||
TEST_CREDENTIALS_INPUT = {
|
||||
"provider": TEST_CREDENTIALS.provider,
|
||||
"id": TEST_CREDENTIALS.id,
|
||||
"type": TEST_CREDENTIALS.type,
|
||||
"title": TEST_CREDENTIALS.title,
|
||||
}
|
||||
SMTPCredentials = UserPasswordCredentials
|
||||
SMTPCredentialsInput = CredentialsMetaInput[
|
||||
Literal[ProviderName.SMTP],
|
||||
Literal["user_password"],
|
||||
]
|
||||
|
||||
|
||||
class EmailCredentials(BaseModel):
|
||||
def SMTPCredentialsField() -> SMTPCredentialsInput:
|
||||
return CredentialsField(
|
||||
description="The SMTP integration requires a username and password.",
|
||||
)
|
||||
|
||||
|
||||
class SMTPConfig(BaseModel):
|
||||
smtp_server: str = SchemaField(
|
||||
default="smtp.gmail.com", description="SMTP server address"
|
||||
default="smtp.example.com", description="SMTP server address"
|
||||
)
|
||||
smtp_port: int = SchemaField(default=25, description="SMTP port number")
|
||||
smtp_username: BlockSecret = SecretField(key="smtp_username")
|
||||
smtp_password: BlockSecret = SecretField(key="smtp_password")
|
||||
|
||||
model_config = ConfigDict(title="Email Credentials")
|
||||
model_config = ConfigDict(title="SMTP Config")
|
||||
|
||||
|
||||
class SendEmailBlock(Block):
|
||||
|
@ -30,10 +61,11 @@ class SendEmailBlock(Block):
|
|||
body: str = SchemaField(
|
||||
description="Body of the email", placeholder="Enter the email body"
|
||||
)
|
||||
creds: EmailCredentials = SchemaField(
|
||||
description="SMTP credentials",
|
||||
default=EmailCredentials(),
|
||||
config: SMTPConfig = SchemaField(
|
||||
description="SMTP Config",
|
||||
default=SMTPConfig(),
|
||||
)
|
||||
credentials: SMTPCredentialsInput = SMTPCredentialsField()
|
||||
|
||||
class Output(BlockSchema):
|
||||
status: str = SchemaField(description="Status of the email sending operation")
|
||||
|
@ -43,7 +75,6 @@ class SendEmailBlock(Block):
|
|||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
disabled=True,
|
||||
id="4335878a-394e-4e67-adf2-919877ff49ae",
|
||||
description="This block sends an email using the provided SMTP credentials.",
|
||||
categories={BlockCategory.OUTPUT},
|
||||
|
@ -53,25 +84,29 @@ class SendEmailBlock(Block):
|
|||
"to_email": "recipient@example.com",
|
||||
"subject": "Test Email",
|
||||
"body": "This is a test email.",
|
||||
"creds": {
|
||||
"config": {
|
||||
"smtp_server": "smtp.gmail.com",
|
||||
"smtp_port": 25,
|
||||
"smtp_username": "your-email@gmail.com",
|
||||
"smtp_password": "your-gmail-password",
|
||||
},
|
||||
"credentials": TEST_CREDENTIALS_INPUT,
|
||||
},
|
||||
test_credentials=TEST_CREDENTIALS,
|
||||
test_output=[("status", "Email sent successfully")],
|
||||
test_mock={"send_email": lambda *args, **kwargs: "Email sent successfully"},
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def send_email(
|
||||
creds: EmailCredentials, to_email: str, subject: str, body: str
|
||||
config: SMTPConfig,
|
||||
to_email: str,
|
||||
subject: str,
|
||||
body: str,
|
||||
credentials: SMTPCredentials,
|
||||
) -> str:
|
||||
smtp_server = creds.smtp_server
|
||||
smtp_port = creds.smtp_port
|
||||
smtp_username = creds.smtp_username.get_secret_value()
|
||||
smtp_password = creds.smtp_password.get_secret_value()
|
||||
smtp_server = config.smtp_server
|
||||
smtp_port = config.smtp_port
|
||||
smtp_username = credentials.username.get_secret_value()
|
||||
smtp_password = credentials.password.get_secret_value()
|
||||
|
||||
msg = MIMEMultipart()
|
||||
msg["From"] = smtp_username
|
||||
|
@ -86,10 +121,13 @@ class SendEmailBlock(Block):
|
|||
|
||||
return "Email sent successfully"
|
||||
|
||||
def run(self, input_data: Input, **kwargs) -> BlockOutput:
|
||||
def run(
|
||||
self, input_data: Input, *, credentials: SMTPCredentials, **kwargs
|
||||
) -> BlockOutput:
|
||||
yield "status", self.send_email(
|
||||
input_data.creds,
|
||||
input_data.to_email,
|
||||
input_data.subject,
|
||||
input_data.body,
|
||||
config=input_data.config,
|
||||
to_email=input_data.to_email,
|
||||
subject=input_data.subject,
|
||||
body=input_data.body,
|
||||
credentials=credentials,
|
||||
)
|
||||
|
|
|
@ -30,7 +30,7 @@ def _convert_to_api_url(url: str) -> str:
|
|||
|
||||
def _get_headers(credentials: GithubCredentials) -> dict[str, str]:
|
||||
return {
|
||||
"Authorization": credentials.bearer(),
|
||||
"Authorization": credentials.auth_header(),
|
||||
"Accept": "application/vnd.github.v3+json",
|
||||
}
|
||||
|
||||
|
|
|
@ -40,7 +40,7 @@ class LinearClient:
|
|||
"Content-Type": "application/json",
|
||||
}
|
||||
if credentials:
|
||||
headers["Authorization"] = credentials.bearer()
|
||||
headers["Authorization"] = credentials.auth_header()
|
||||
|
||||
self._requests = Requests(
|
||||
extra_headers=headers,
|
||||
|
|
|
@ -1,22 +1,48 @@
|
|||
from datetime import datetime, timezone
|
||||
from typing import Iterator
|
||||
from typing import Iterator, Literal
|
||||
|
||||
import praw
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from pydantic import BaseModel, SecretStr
|
||||
|
||||
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
|
||||
from backend.data.model import BlockSecret, SchemaField, SecretField
|
||||
from backend.data.model import (
|
||||
CredentialsField,
|
||||
CredentialsMetaInput,
|
||||
SchemaField,
|
||||
UserPasswordCredentials,
|
||||
)
|
||||
from backend.integrations.providers import ProviderName
|
||||
from backend.util.mock import MockObject
|
||||
from backend.util.settings import Settings
|
||||
|
||||
RedditCredentials = UserPasswordCredentials
|
||||
RedditCredentialsInput = CredentialsMetaInput[
|
||||
Literal[ProviderName.REDDIT],
|
||||
Literal["user_password"],
|
||||
]
|
||||
|
||||
|
||||
class RedditCredentials(BaseModel):
|
||||
client_id: BlockSecret = SecretField(key="reddit_client_id")
|
||||
client_secret: BlockSecret = SecretField(key="reddit_client_secret")
|
||||
username: BlockSecret = SecretField(key="reddit_username")
|
||||
password: BlockSecret = SecretField(key="reddit_password")
|
||||
user_agent: str = "AutoGPT:1.0 (by /u/autogpt)"
|
||||
def RedditCredentialsField() -> RedditCredentialsInput:
|
||||
"""Creates a Reddit credentials input on a block."""
|
||||
return CredentialsField(
|
||||
description="The Reddit integration requires a username and password.",
|
||||
)
|
||||
|
||||
model_config = ConfigDict(title="Reddit Credentials")
|
||||
|
||||
TEST_CREDENTIALS = UserPasswordCredentials(
|
||||
id="01234567-89ab-cdef-0123-456789abcdef",
|
||||
provider="reddit",
|
||||
username=SecretStr("mock-reddit-username"),
|
||||
password=SecretStr("mock-reddit-password"),
|
||||
title="Mock Reddit credentials",
|
||||
)
|
||||
|
||||
TEST_CREDENTIALS_INPUT = {
|
||||
"provider": TEST_CREDENTIALS.provider,
|
||||
"id": TEST_CREDENTIALS.id,
|
||||
"type": TEST_CREDENTIALS.type,
|
||||
"title": TEST_CREDENTIALS.title,
|
||||
}
|
||||
|
||||
|
||||
class RedditPost(BaseModel):
|
||||
|
@ -31,13 +57,16 @@ class RedditComment(BaseModel):
|
|||
comment: str
|
||||
|
||||
|
||||
settings = Settings()
|
||||
|
||||
|
||||
def get_praw(creds: RedditCredentials) -> praw.Reddit:
|
||||
client = praw.Reddit(
|
||||
client_id=creds.client_id.get_secret_value(),
|
||||
client_secret=creds.client_secret.get_secret_value(),
|
||||
client_id=settings.secrets.reddit_client_id,
|
||||
client_secret=settings.secrets.reddit_client_secret,
|
||||
username=creds.username.get_secret_value(),
|
||||
password=creds.password.get_secret_value(),
|
||||
user_agent=creds.user_agent,
|
||||
user_agent=settings.config.reddit_user_agent,
|
||||
)
|
||||
me = client.user.me()
|
||||
if not me:
|
||||
|
@ -48,11 +77,11 @@ def get_praw(creds: RedditCredentials) -> praw.Reddit:
|
|||
|
||||
class GetRedditPostsBlock(Block):
|
||||
class Input(BlockSchema):
|
||||
subreddit: str = SchemaField(description="Subreddit name")
|
||||
creds: RedditCredentials = SchemaField(
|
||||
description="Reddit credentials",
|
||||
default=RedditCredentials(),
|
||||
subreddit: str = SchemaField(
|
||||
description="Subreddit name, excluding the /r/ prefix",
|
||||
default="writingprompts",
|
||||
)
|
||||
credentials: RedditCredentialsInput = RedditCredentialsField()
|
||||
last_minutes: int | None = SchemaField(
|
||||
description="Post time to stop minutes ago while fetching posts",
|
||||
default=None,
|
||||
|
@ -70,20 +99,18 @@ class GetRedditPostsBlock(Block):
|
|||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
disabled=True,
|
||||
id="c6731acb-4285-4ee1-bc9b-03d0766c370f",
|
||||
description="This block fetches Reddit posts from a defined subreddit name.",
|
||||
categories={BlockCategory.SOCIAL},
|
||||
disabled=(
|
||||
not settings.secrets.reddit_client_id
|
||||
or not settings.secrets.reddit_client_secret
|
||||
),
|
||||
input_schema=GetRedditPostsBlock.Input,
|
||||
output_schema=GetRedditPostsBlock.Output,
|
||||
test_credentials=TEST_CREDENTIALS,
|
||||
test_input={
|
||||
"creds": {
|
||||
"client_id": "client_id",
|
||||
"client_secret": "client_secret",
|
||||
"username": "username",
|
||||
"password": "password",
|
||||
"user_agent": "user_agent",
|
||||
},
|
||||
"credentials": TEST_CREDENTIALS_INPUT,
|
||||
"subreddit": "subreddit",
|
||||
"last_post": "id3",
|
||||
"post_limit": 2,
|
||||
|
@ -103,7 +130,7 @@ class GetRedditPostsBlock(Block):
|
|||
),
|
||||
],
|
||||
test_mock={
|
||||
"get_posts": lambda _: [
|
||||
"get_posts": lambda input_data, credentials: [
|
||||
MockObject(id="id1", title="title1", selftext="body1"),
|
||||
MockObject(id="id2", title="title2", selftext="body2"),
|
||||
MockObject(id="id3", title="title2", selftext="body2"),
|
||||
|
@ -112,14 +139,18 @@ class GetRedditPostsBlock(Block):
|
|||
)
|
||||
|
||||
@staticmethod
|
||||
def get_posts(input_data: Input) -> Iterator[praw.reddit.Submission]:
|
||||
client = get_praw(input_data.creds)
|
||||
def get_posts(
|
||||
input_data: Input, *, credentials: RedditCredentials
|
||||
) -> Iterator[praw.reddit.Submission]:
|
||||
client = get_praw(credentials)
|
||||
subreddit = client.subreddit(input_data.subreddit)
|
||||
return subreddit.new(limit=input_data.post_limit or 10)
|
||||
|
||||
def run(self, input_data: Input, **kwargs) -> BlockOutput:
|
||||
def run(
|
||||
self, input_data: Input, *, credentials: RedditCredentials, **kwargs
|
||||
) -> BlockOutput:
|
||||
current_time = datetime.now(tz=timezone.utc)
|
||||
for post in self.get_posts(input_data):
|
||||
for post in self.get_posts(input_data=input_data, credentials=credentials):
|
||||
if input_data.last_minutes:
|
||||
post_datetime = datetime.fromtimestamp(
|
||||
post.created_utc, tz=timezone.utc
|
||||
|
@ -141,9 +172,7 @@ class GetRedditPostsBlock(Block):
|
|||
|
||||
class PostRedditCommentBlock(Block):
|
||||
class Input(BlockSchema):
|
||||
creds: RedditCredentials = SchemaField(
|
||||
description="Reddit credentials", default=RedditCredentials()
|
||||
)
|
||||
credentials: RedditCredentialsInput = RedditCredentialsField()
|
||||
data: RedditComment = SchemaField(description="Reddit comment")
|
||||
|
||||
class Output(BlockSchema):
|
||||
|
@ -156,7 +185,15 @@ class PostRedditCommentBlock(Block):
|
|||
categories={BlockCategory.SOCIAL},
|
||||
input_schema=PostRedditCommentBlock.Input,
|
||||
output_schema=PostRedditCommentBlock.Output,
|
||||
test_input={"data": {"post_id": "id", "comment": "comment"}},
|
||||
disabled=(
|
||||
not settings.secrets.reddit_client_id
|
||||
or not settings.secrets.reddit_client_secret
|
||||
),
|
||||
test_credentials=TEST_CREDENTIALS,
|
||||
test_input={
|
||||
"credentials": TEST_CREDENTIALS_INPUT,
|
||||
"data": {"post_id": "id", "comment": "comment"},
|
||||
},
|
||||
test_output=[("comment_id", "dummy_comment_id")],
|
||||
test_mock={"reply_post": lambda creds, comment: "dummy_comment_id"},
|
||||
)
|
||||
|
@ -170,5 +207,7 @@ class PostRedditCommentBlock(Block):
|
|||
raise ValueError("Failed to post comment.")
|
||||
return new_comment.id
|
||||
|
||||
def run(self, input_data: Input, **kwargs) -> BlockOutput:
|
||||
yield "comment_id", self.reply_post(input_data.creds, input_data.data)
|
||||
def run(
|
||||
self, input_data: Input, *, credentials: RedditCredentials, **kwargs
|
||||
) -> BlockOutput:
|
||||
yield "comment_id", self.reply_post(credentials, input_data.data)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import logging
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
|
@ -199,27 +200,42 @@ class OAuth2Credentials(_BaseCredentials):
|
|||
scopes: list[str]
|
||||
metadata: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
def bearer(self) -> str:
|
||||
def auth_header(self) -> str:
|
||||
return f"Bearer {self.access_token.get_secret_value()}"
|
||||
|
||||
|
||||
class APIKeyCredentials(_BaseCredentials):
|
||||
type: Literal["api_key"] = "api_key"
|
||||
api_key: SecretStr
|
||||
expires_at: Optional[int]
|
||||
expires_at: Optional[int] = Field(
|
||||
default=None,
|
||||
description="Unix timestamp (seconds) indicating when the API key expires (if at all)",
|
||||
)
|
||||
"""Unix timestamp (seconds) indicating when the API key expires (if at all)"""
|
||||
|
||||
def bearer(self) -> str:
|
||||
def auth_header(self) -> str:
|
||||
return f"Bearer {self.api_key.get_secret_value()}"
|
||||
|
||||
|
||||
class UserPasswordCredentials(_BaseCredentials):
|
||||
type: Literal["user_password"] = "user_password"
|
||||
username: SecretStr
|
||||
password: SecretStr
|
||||
|
||||
def auth_header(self) -> str:
|
||||
# Converting the string to bytes using encode()
|
||||
# Base64 encoding it with base64.b64encode()
|
||||
# Converting the resulting bytes back to a string with decode()
|
||||
return f"Basic {base64.b64encode(f'{self.username.get_secret_value()}:{self.password.get_secret_value()}'.encode()).decode()}"
|
||||
|
||||
|
||||
Credentials = Annotated[
|
||||
OAuth2Credentials | APIKeyCredentials,
|
||||
OAuth2Credentials | APIKeyCredentials | UserPasswordCredentials,
|
||||
Field(discriminator="type"),
|
||||
]
|
||||
|
||||
|
||||
CredentialsType = Literal["api_key", "oauth2"]
|
||||
CredentialsType = Literal["api_key", "oauth2", "user_password"]
|
||||
|
||||
|
||||
class OAuthState(BaseModel):
|
||||
|
|
|
@ -26,9 +26,11 @@ class ProviderName(str, Enum):
|
|||
OPENWEATHERMAP = "openweathermap"
|
||||
OPEN_ROUTER = "open_router"
|
||||
PINECONE = "pinecone"
|
||||
REDDIT = "reddit"
|
||||
REPLICATE = "replicate"
|
||||
REVID = "revid"
|
||||
SLANT3D = "slant3d"
|
||||
SMTP = "smtp"
|
||||
TWITTER = "twitter"
|
||||
UNREAL_SPEECH = "unreal_speech"
|
||||
# --8<-- [end:ProviderName]
|
||||
|
|
|
@ -168,7 +168,7 @@ class BaseWebhooksManager(ABC, Generic[WT]):
|
|||
|
||||
id = str(uuid4())
|
||||
secret = secrets.token_hex(32)
|
||||
provider_name = self.PROVIDER_NAME
|
||||
provider_name: ProviderName = self.PROVIDER_NAME
|
||||
ingress_url = webhook_ingress_url(provider_name=provider_name, webhook_id=id)
|
||||
if register:
|
||||
if not credentials:
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import logging
|
||||
|
||||
from backend.data import integrations
|
||||
from backend.data.model import APIKeyCredentials, Credentials, OAuth2Credentials
|
||||
from backend.data.model import Credentials
|
||||
|
||||
from ._base import WT, BaseWebhooksManager
|
||||
|
||||
|
@ -25,6 +25,6 @@ class ManualWebhookManagerBase(BaseWebhooksManager[WT]):
|
|||
async def _deregister_webhook(
|
||||
self,
|
||||
webhook: integrations.Webhook,
|
||||
credentials: OAuth2Credentials | APIKeyCredentials,
|
||||
credentials: Credentials,
|
||||
) -> None:
|
||||
pass
|
||||
|
|
|
@ -67,7 +67,7 @@ class GithubWebhooksManager(BaseWebhooksManager):
|
|||
|
||||
headers = {
|
||||
**self.GITHUB_API_DEFAULT_HEADERS,
|
||||
"Authorization": credentials.bearer(),
|
||||
"Authorization": credentials.auth_header(),
|
||||
}
|
||||
|
||||
repo, github_hook_id = webhook.resource, webhook.provider_webhook_id
|
||||
|
@ -96,7 +96,7 @@ class GithubWebhooksManager(BaseWebhooksManager):
|
|||
|
||||
headers = {
|
||||
**self.GITHUB_API_DEFAULT_HEADERS,
|
||||
"Authorization": credentials.bearer(),
|
||||
"Authorization": credentials.auth_header(),
|
||||
}
|
||||
webhook_data = {
|
||||
"name": "web",
|
||||
|
@ -142,7 +142,7 @@ class GithubWebhooksManager(BaseWebhooksManager):
|
|||
|
||||
headers = {
|
||||
**self.GITHUB_API_DEFAULT_HEADERS,
|
||||
"Authorization": credentials.bearer(),
|
||||
"Authorization": credentials.auth_header(),
|
||||
}
|
||||
|
||||
if webhook_type == self.WebhookType.REPO:
|
||||
|
|
|
@ -2,7 +2,7 @@ import logging
|
|||
from typing import TYPE_CHECKING, Annotated, Literal
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, HTTPException, Path, Query, Request
|
||||
from pydantic import BaseModel, Field, SecretStr
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from backend.data.graph import set_node_webhook
|
||||
from backend.data.integrations import (
|
||||
|
@ -12,12 +12,7 @@ from backend.data.integrations import (
|
|||
publish_webhook_event,
|
||||
wait_for_webhook_event,
|
||||
)
|
||||
from backend.data.model import (
|
||||
APIKeyCredentials,
|
||||
Credentials,
|
||||
CredentialsType,
|
||||
OAuth2Credentials,
|
||||
)
|
||||
from backend.data.model import Credentials, CredentialsType, OAuth2Credentials
|
||||
from backend.executor.manager import ExecutionManager
|
||||
from backend.integrations.creds_manager import IntegrationCredentialsManager
|
||||
from backend.integrations.oauth import HANDLERS_BY_NAME
|
||||
|
@ -204,31 +199,21 @@ def get_credential(
|
|||
|
||||
|
||||
@router.post("/{provider}/credentials", status_code=201)
|
||||
def create_api_key_credentials(
|
||||
def create_credentials(
|
||||
user_id: Annotated[str, Depends(get_user_id)],
|
||||
provider: Annotated[
|
||||
ProviderName, Path(title="The provider to create credentials for")
|
||||
],
|
||||
api_key: Annotated[str, Body(title="The API key to store")],
|
||||
title: Annotated[str, Body(title="Optional title for the credentials")],
|
||||
expires_at: Annotated[
|
||||
int | None, Body(title="Unix timestamp when the key expires")
|
||||
] = None,
|
||||
) -> APIKeyCredentials:
|
||||
new_credentials = APIKeyCredentials(
|
||||
provider=provider,
|
||||
api_key=SecretStr(api_key),
|
||||
title=title,
|
||||
expires_at=expires_at,
|
||||
)
|
||||
|
||||
credentials: Credentials,
|
||||
) -> Credentials:
|
||||
credentials.provider = provider
|
||||
try:
|
||||
creds_manager.create(user_id, new_credentials)
|
||||
creds_manager.create(user_id, credentials)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Failed to store credentials: {str(e)}"
|
||||
)
|
||||
return new_credentials
|
||||
return credentials
|
||||
|
||||
|
||||
class CredentialsDeletionResponse(BaseModel):
|
||||
|
|
|
@ -157,6 +157,11 @@ class Config(UpdateTrackingModel["Config"], BaseSettings):
|
|||
description="The name of the Google Cloud Storage bucket for media files",
|
||||
)
|
||||
|
||||
reddit_user_agent: str = Field(
|
||||
default="AutoGPT:1.0 (by /u/autogpt)",
|
||||
description="The user agent for the Reddit API",
|
||||
)
|
||||
|
||||
scheduler_db_pool_size: int = Field(
|
||||
default=3,
|
||||
description="The pool size for the scheduler database connection pool",
|
||||
|
@ -280,8 +285,6 @@ class Secrets(UpdateTrackingModel["Secrets"], BaseSettings):
|
|||
|
||||
reddit_client_id: str = Field(default="", description="Reddit client ID")
|
||||
reddit_client_secret: str = Field(default="", description="Reddit client secret")
|
||||
reddit_username: str = Field(default="", description="Reddit username")
|
||||
reddit_password: str = Field(default="", description="Reddit password")
|
||||
|
||||
openweathermap_api_key: str = Field(
|
||||
default="", description="OpenWeatherMap API key"
|
||||
|
|
|
@ -124,14 +124,22 @@ export default function PrivatePage() {
|
|||
|
||||
const allCredentials = providers
|
||||
? Object.values(providers).flatMap((provider) =>
|
||||
[...provider.savedOAuthCredentials, ...provider.savedApiKeys]
|
||||
[
|
||||
...provider.savedOAuthCredentials,
|
||||
...provider.savedApiKeys,
|
||||
...provider.savedUserPasswordCredentials,
|
||||
]
|
||||
.filter((cred) => !hiddenCredentials.includes(cred.id))
|
||||
.map((credentials) => ({
|
||||
...credentials,
|
||||
provider: provider.provider,
|
||||
providerName: provider.providerName,
|
||||
ProviderIcon: providerIcons[provider.provider],
|
||||
TypeIcon: { oauth2: IconUser, api_key: IconKey }[credentials.type],
|
||||
TypeIcon: {
|
||||
oauth2: IconUser,
|
||||
api_key: IconKey,
|
||||
user_password: IconKey,
|
||||
}[credentials.type],
|
||||
})),
|
||||
)
|
||||
: [];
|
||||
|
|
|
@ -124,14 +124,22 @@ export default function PrivatePage() {
|
|||
|
||||
const allCredentials = providers
|
||||
? Object.values(providers).flatMap((provider) =>
|
||||
[...provider.savedOAuthCredentials, ...provider.savedApiKeys]
|
||||
[
|
||||
...provider.savedOAuthCredentials,
|
||||
...provider.savedApiKeys,
|
||||
...provider.savedUserPasswordCredentials,
|
||||
]
|
||||
.filter((cred) => !hiddenCredentials.includes(cred.id))
|
||||
.map((credentials) => ({
|
||||
...credentials,
|
||||
provider: provider.provider,
|
||||
providerName: provider.providerName,
|
||||
ProviderIcon: providerIcons[provider.provider],
|
||||
TypeIcon: { oauth2: IconUser, api_key: IconKey }[credentials.type],
|
||||
TypeIcon: {
|
||||
oauth2: IconUser,
|
||||
api_key: IconKey,
|
||||
user_password: IconKey,
|
||||
}[credentials.type],
|
||||
})),
|
||||
)
|
||||
: [];
|
||||
|
@ -176,6 +184,7 @@ export default function PrivatePage() {
|
|||
{
|
||||
oauth2: "OAuth2 credentials",
|
||||
api_key: "API key",
|
||||
user_password: "User password",
|
||||
}[cred.type]
|
||||
}{" "}
|
||||
- <code>{cred.id}</code>
|
||||
|
|
|
@ -75,7 +75,9 @@ export const providerIcons: Record<
|
|||
open_router: fallbackIcon,
|
||||
pinecone: fallbackIcon,
|
||||
slant3d: fallbackIcon,
|
||||
smtp: fallbackIcon,
|
||||
replicate: fallbackIcon,
|
||||
reddit: fallbackIcon,
|
||||
fal: fallbackIcon,
|
||||
revid: fallbackIcon,
|
||||
twitter: FaTwitter,
|
||||
|
@ -107,6 +109,10 @@ export const CredentialsInput: FC<{
|
|||
const credentials = useCredentials(selfKey);
|
||||
const [isAPICredentialsModalOpen, setAPICredentialsModalOpen] =
|
||||
useState(false);
|
||||
const [
|
||||
isUserPasswordCredentialsModalOpen,
|
||||
setUserPasswordCredentialsModalOpen,
|
||||
] = useState(false);
|
||||
const [isOAuth2FlowInProgress, setOAuth2FlowInProgress] = useState(false);
|
||||
const [oAuthPopupController, setOAuthPopupController] =
|
||||
useState<AbortController | null>(null);
|
||||
|
@ -122,8 +128,10 @@ export const CredentialsInput: FC<{
|
|||
providerName,
|
||||
supportsApiKey,
|
||||
supportsOAuth2,
|
||||
supportsUserPassword,
|
||||
savedApiKeys,
|
||||
savedOAuthCredentials,
|
||||
savedUserPasswordCredentials,
|
||||
oAuthCallback,
|
||||
} = credentials;
|
||||
|
||||
|
@ -237,6 +245,17 @@ export const CredentialsInput: FC<{
|
|||
providerName={providerName}
|
||||
/>
|
||||
)}
|
||||
{supportsUserPassword && (
|
||||
<UserPasswordCredentialsModal
|
||||
credentialsFieldName={selfKey}
|
||||
open={isUserPasswordCredentialsModalOpen}
|
||||
onClose={() => setUserPasswordCredentialsModalOpen(false)}
|
||||
onCredentialsCreate={(creds) => {
|
||||
onSelectCredentials(creds);
|
||||
setUserPasswordCredentialsModalOpen(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
|
@ -245,13 +264,18 @@ export const CredentialsInput: FC<{
|
|||
selectedCredentials &&
|
||||
!savedApiKeys
|
||||
.concat(savedOAuthCredentials)
|
||||
.concat(savedUserPasswordCredentials)
|
||||
.some((c) => c.id === selectedCredentials.id)
|
||||
) {
|
||||
onSelectCredentials(undefined);
|
||||
}
|
||||
|
||||
// No saved credentials yet
|
||||
if (savedApiKeys.length === 0 && savedOAuthCredentials.length === 0) {
|
||||
if (
|
||||
savedApiKeys.length === 0 &&
|
||||
savedOAuthCredentials.length === 0 &&
|
||||
savedUserPasswordCredentials.length === 0
|
||||
) {
|
||||
return (
|
||||
<>
|
||||
<div className="mb-2 flex gap-1">
|
||||
|
@ -273,6 +297,12 @@ export const CredentialsInput: FC<{
|
|||
Enter API key
|
||||
</Button>
|
||||
)}
|
||||
{supportsUserPassword && (
|
||||
<Button onClick={() => setUserPasswordCredentialsModalOpen(true)}>
|
||||
<ProviderIcon className="mr-2 h-4 w-4" />
|
||||
Enter username and password
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{modals}
|
||||
{oAuthError && (
|
||||
|
@ -282,12 +312,29 @@ export const CredentialsInput: FC<{
|
|||
);
|
||||
}
|
||||
|
||||
const singleCredential =
|
||||
savedApiKeys.length === 1 && savedOAuthCredentials.length === 0
|
||||
? savedApiKeys[0]
|
||||
: savedOAuthCredentials.length === 1 && savedApiKeys.length === 0
|
||||
? savedOAuthCredentials[0]
|
||||
: null;
|
||||
const getCredentialCounts = () => ({
|
||||
apiKeys: savedApiKeys.length,
|
||||
oauth: savedOAuthCredentials.length,
|
||||
userPass: savedUserPasswordCredentials.length,
|
||||
});
|
||||
|
||||
const getSingleCredential = () => {
|
||||
const counts = getCredentialCounts();
|
||||
const totalCredentials = Object.values(counts).reduce(
|
||||
(sum, count) => sum + count,
|
||||
0,
|
||||
);
|
||||
|
||||
if (totalCredentials !== 1) return null;
|
||||
|
||||
if (counts.apiKeys === 1) return savedApiKeys[0];
|
||||
if (counts.oauth === 1) return savedOAuthCredentials[0];
|
||||
if (counts.userPass === 1) return savedUserPasswordCredentials[0];
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const singleCredential = getSingleCredential();
|
||||
|
||||
if (singleCredential) {
|
||||
if (!selectedCredentials) {
|
||||
|
@ -311,6 +358,7 @@ export const CredentialsInput: FC<{
|
|||
} else {
|
||||
const selectedCreds = savedApiKeys
|
||||
.concat(savedOAuthCredentials)
|
||||
.concat(savedUserPasswordCredentials)
|
||||
.find((c) => c.id == newValue)!;
|
||||
|
||||
onSelectCredentials({
|
||||
|
@ -349,6 +397,13 @@ export const CredentialsInput: FC<{
|
|||
{credentials.title}
|
||||
</SelectItem>
|
||||
))}
|
||||
{savedUserPasswordCredentials.map((credentials, index) => (
|
||||
<SelectItem key={index} value={credentials.id}>
|
||||
<ProviderIcon className="mr-2 inline h-4 w-4" />
|
||||
<IconUserPlus className="mr-1.5 inline" />
|
||||
{credentials.title}
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectSeparator />
|
||||
{supportsOAuth2 && (
|
||||
<SelectItem value="sign-in">
|
||||
|
@ -362,6 +417,12 @@ export const CredentialsInput: FC<{
|
|||
Add new API key
|
||||
</SelectItem>
|
||||
)}
|
||||
{supportsUserPassword && (
|
||||
<SelectItem value="add-user-password">
|
||||
<IconUserPlus className="mr-1.5 inline" />
|
||||
Add new user password
|
||||
</SelectItem>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{modals}
|
||||
|
@ -508,6 +569,130 @@ export const APIKeyCredentialsModal: FC<{
|
|||
);
|
||||
};
|
||||
|
||||
export const UserPasswordCredentialsModal: FC<{
|
||||
credentialsFieldName: string;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onCredentialsCreate: (creds: CredentialsMetaInput) => void;
|
||||
}> = ({ credentialsFieldName, open, onClose, onCredentialsCreate }) => {
|
||||
const credentials = useCredentials(credentialsFieldName);
|
||||
|
||||
const formSchema = z.object({
|
||||
username: z.string().min(1, "Username is required"),
|
||||
password: z.string().min(1, "Password is required"),
|
||||
title: z.string().min(1, "Name is required"),
|
||||
});
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
username: "",
|
||||
password: "",
|
||||
title: "",
|
||||
},
|
||||
});
|
||||
|
||||
if (
|
||||
!credentials ||
|
||||
credentials.isLoading ||
|
||||
!credentials.supportsUserPassword
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { schema, provider, providerName, createUserPasswordCredentials } =
|
||||
credentials;
|
||||
|
||||
async function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
const newCredentials = await createUserPasswordCredentials({
|
||||
username: values.username,
|
||||
password: values.password,
|
||||
title: values.title,
|
||||
});
|
||||
onCredentialsCreate({
|
||||
provider,
|
||||
id: newCredentials.id,
|
||||
type: "user_password",
|
||||
title: newCredentials.title,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) onClose();
|
||||
}}
|
||||
>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
Add new username & password for {providerName}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="username"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Username</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Enter username..."
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Enter password..."
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="title"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Enter a name for this user login..."
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit" className="w-full">
|
||||
Save & use this user login
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export const OAuth2FlowWaitingModal: FC<{
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
|
|
|
@ -5,6 +5,7 @@ import {
|
|||
CredentialsMetaResponse,
|
||||
CredentialsProviderName,
|
||||
PROVIDER_NAMES,
|
||||
UserPasswordCredentials,
|
||||
} from "@/lib/autogpt-server-api";
|
||||
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||
import { createContext, useCallback, useEffect, useState } from "react";
|
||||
|
@ -20,10 +21,13 @@ const providerDisplayNames: Record<CredentialsProviderName, string> = {
|
|||
discord: "Discord",
|
||||
d_id: "D-ID",
|
||||
e2b: "E2B",
|
||||
exa: "Exa",
|
||||
fal: "FAL",
|
||||
github: "GitHub",
|
||||
google: "Google",
|
||||
google_maps: "Google Maps",
|
||||
groq: "Groq",
|
||||
hubspot: "Hubspot",
|
||||
ideogram: "Ideogram",
|
||||
jina: "Jina",
|
||||
linear: "Linear",
|
||||
|
@ -36,13 +40,12 @@ const providerDisplayNames: Record<CredentialsProviderName, string> = {
|
|||
open_router: "Open Router",
|
||||
pinecone: "Pinecone",
|
||||
slant3d: "Slant3D",
|
||||
smtp: "SMTP",
|
||||
reddit: "Reddit",
|
||||
replicate: "Replicate",
|
||||
fal: "FAL",
|
||||
revid: "Rev.ID",
|
||||
twitter: "Twitter",
|
||||
unreal_speech: "Unreal Speech",
|
||||
exa: "Exa",
|
||||
hubspot: "Hubspot",
|
||||
} as const;
|
||||
// --8<-- [end:CredentialsProviderNames]
|
||||
|
||||
|
@ -51,11 +54,17 @@ type APIKeyCredentialsCreatable = Omit<
|
|||
"id" | "provider" | "type"
|
||||
>;
|
||||
|
||||
type UserPasswordCredentialsCreatable = Omit<
|
||||
UserPasswordCredentials,
|
||||
"id" | "provider" | "type"
|
||||
>;
|
||||
|
||||
export type CredentialsProviderData = {
|
||||
provider: CredentialsProviderName;
|
||||
providerName: string;
|
||||
savedApiKeys: CredentialsMetaResponse[];
|
||||
savedOAuthCredentials: CredentialsMetaResponse[];
|
||||
savedUserPasswordCredentials: CredentialsMetaResponse[];
|
||||
oAuthCallback: (
|
||||
code: string,
|
||||
state_token: string,
|
||||
|
@ -63,6 +72,9 @@ export type CredentialsProviderData = {
|
|||
createAPIKeyCredentials: (
|
||||
credentials: APIKeyCredentialsCreatable,
|
||||
) => Promise<CredentialsMetaResponse>;
|
||||
createUserPasswordCredentials: (
|
||||
credentials: UserPasswordCredentialsCreatable,
|
||||
) => Promise<CredentialsMetaResponse>;
|
||||
deleteCredentials: (
|
||||
id: string,
|
||||
force?: boolean,
|
||||
|
@ -107,6 +119,11 @@ export default function CredentialsProvider({
|
|||
...updatedProvider.savedOAuthCredentials,
|
||||
credentials,
|
||||
];
|
||||
} else if (credentials.type === "user_password") {
|
||||
updatedProvider.savedUserPasswordCredentials = [
|
||||
...updatedProvider.savedUserPasswordCredentials,
|
||||
credentials,
|
||||
];
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -148,6 +165,22 @@ export default function CredentialsProvider({
|
|||
[api, addCredentials],
|
||||
);
|
||||
|
||||
/** Wraps `BackendAPI.createUserPasswordCredentials`, and adds the result to the internal credentials store. */
|
||||
const createUserPasswordCredentials = useCallback(
|
||||
async (
|
||||
provider: CredentialsProviderName,
|
||||
credentials: UserPasswordCredentialsCreatable,
|
||||
): Promise<CredentialsMetaResponse> => {
|
||||
const credsMeta = await api.createUserPasswordCredentials({
|
||||
provider,
|
||||
...credentials,
|
||||
});
|
||||
addCredentials(provider, credsMeta);
|
||||
return credsMeta;
|
||||
},
|
||||
[api, addCredentials],
|
||||
);
|
||||
|
||||
/** Wraps `BackendAPI.deleteCredentials`, and removes the credentials from the internal store. */
|
||||
const deleteCredentials = useCallback(
|
||||
async (
|
||||
|
@ -172,7 +205,10 @@ export default function CredentialsProvider({
|
|||
updatedProvider.savedOAuthCredentials.filter(
|
||||
(cred) => cred.id !== id,
|
||||
);
|
||||
|
||||
updatedProvider.savedUserPasswordCredentials =
|
||||
updatedProvider.savedUserPasswordCredentials.filter(
|
||||
(cred) => cred.id !== id,
|
||||
);
|
||||
return {
|
||||
...prev,
|
||||
[provider]: updatedProvider,
|
||||
|
@ -191,12 +227,18 @@ export default function CredentialsProvider({
|
|||
const credentialsByProvider = response.reduce(
|
||||
(acc, cred) => {
|
||||
if (!acc[cred.provider]) {
|
||||
acc[cred.provider] = { oauthCreds: [], apiKeys: [] };
|
||||
acc[cred.provider] = {
|
||||
oauthCreds: [],
|
||||
apiKeys: [],
|
||||
userPasswordCreds: [],
|
||||
};
|
||||
}
|
||||
if (cred.type === "oauth2") {
|
||||
acc[cred.provider].oauthCreds.push(cred);
|
||||
} else if (cred.type === "api_key") {
|
||||
acc[cred.provider].apiKeys.push(cred);
|
||||
} else if (cred.type === "user_password") {
|
||||
acc[cred.provider].userPasswordCreds.push(cred);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
|
@ -205,6 +247,7 @@ export default function CredentialsProvider({
|
|||
{
|
||||
oauthCreds: CredentialsMetaResponse[];
|
||||
apiKeys: CredentialsMetaResponse[];
|
||||
userPasswordCreds: CredentialsMetaResponse[];
|
||||
}
|
||||
>,
|
||||
);
|
||||
|
@ -221,6 +264,8 @@ export default function CredentialsProvider({
|
|||
savedApiKeys: credentialsByProvider[provider]?.apiKeys ?? [],
|
||||
savedOAuthCredentials:
|
||||
credentialsByProvider[provider]?.oauthCreds ?? [],
|
||||
savedUserPasswordCredentials:
|
||||
credentialsByProvider[provider]?.userPasswordCreds ?? [],
|
||||
oAuthCallback: (code: string, state_token: string) =>
|
||||
oAuthCallback(
|
||||
provider as CredentialsProviderName,
|
||||
|
@ -234,6 +279,13 @@ export default function CredentialsProvider({
|
|||
provider as CredentialsProviderName,
|
||||
credentials,
|
||||
),
|
||||
createUserPasswordCredentials: (
|
||||
credentials: UserPasswordCredentialsCreatable,
|
||||
) =>
|
||||
createUserPasswordCredentials(
|
||||
provider as CredentialsProviderName,
|
||||
credentials,
|
||||
),
|
||||
deleteCredentials: (id: string, force: boolean = false) =>
|
||||
deleteCredentials(
|
||||
provider as CredentialsProviderName,
|
||||
|
@ -246,7 +298,13 @@ export default function CredentialsProvider({
|
|||
}));
|
||||
});
|
||||
});
|
||||
}, [api, createAPIKeyCredentials, deleteCredentials, oAuthCallback]);
|
||||
}, [
|
||||
api,
|
||||
createAPIKeyCredentials,
|
||||
createUserPasswordCredentials,
|
||||
deleteCredentials,
|
||||
oAuthCallback,
|
||||
]);
|
||||
|
||||
return (
|
||||
<CredentialsProvidersContext.Provider value={providers}>
|
||||
|
|
|
@ -17,12 +17,14 @@ export type CredentialsData =
|
|||
schema: BlockIOCredentialsSubSchema;
|
||||
supportsApiKey: boolean;
|
||||
supportsOAuth2: boolean;
|
||||
supportsUserPassword: boolean;
|
||||
isLoading: true;
|
||||
}
|
||||
| (CredentialsProviderData & {
|
||||
schema: BlockIOCredentialsSubSchema;
|
||||
supportsApiKey: boolean;
|
||||
supportsOAuth2: boolean;
|
||||
supportsUserPassword: boolean;
|
||||
isLoading: false;
|
||||
});
|
||||
|
||||
|
@ -72,6 +74,8 @@ export default function useCredentials(
|
|||
const supportsApiKey =
|
||||
credentialsSchema.credentials_types.includes("api_key");
|
||||
const supportsOAuth2 = credentialsSchema.credentials_types.includes("oauth2");
|
||||
const supportsUserPassword =
|
||||
credentialsSchema.credentials_types.includes("user_password");
|
||||
|
||||
// No provider means maybe it's still loading
|
||||
if (!provider) {
|
||||
|
@ -93,13 +97,17 @@ export default function useCredentials(
|
|||
)
|
||||
: provider.savedOAuthCredentials;
|
||||
|
||||
const savedUserPasswordCredentials = provider.savedUserPasswordCredentials;
|
||||
|
||||
return {
|
||||
...provider,
|
||||
provider: providerName,
|
||||
schema: credentialsSchema,
|
||||
supportsApiKey,
|
||||
supportsOAuth2,
|
||||
supportsUserPassword,
|
||||
savedOAuthCredentials,
|
||||
savedUserPasswordCredentials,
|
||||
isLoading: false,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -15,7 +15,6 @@ import {
|
|||
GraphUpdateable,
|
||||
NodeExecutionResult,
|
||||
MyAgentsResponse,
|
||||
OAuth2Credentials,
|
||||
ProfileDetails,
|
||||
User,
|
||||
StoreAgentsResponse,
|
||||
|
@ -29,6 +28,8 @@ import {
|
|||
StoreReview,
|
||||
ScheduleCreatable,
|
||||
Schedule,
|
||||
UserPasswordCredentials,
|
||||
Credentials,
|
||||
APIKeyPermission,
|
||||
CreateAPIKeyResponse,
|
||||
APIKey,
|
||||
|
@ -203,7 +204,17 @@ export default class BackendAPI {
|
|||
return this._request(
|
||||
"POST",
|
||||
`/integrations/${credentials.provider}/credentials`,
|
||||
credentials,
|
||||
{ ...credentials, type: "api_key" },
|
||||
);
|
||||
}
|
||||
|
||||
createUserPasswordCredentials(
|
||||
credentials: Omit<UserPasswordCredentials, "id" | "type">,
|
||||
): Promise<UserPasswordCredentials> {
|
||||
return this._request(
|
||||
"POST",
|
||||
`/integrations/${credentials.provider}/credentials`,
|
||||
{ ...credentials, type: "user_password" },
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -215,10 +226,7 @@ export default class BackendAPI {
|
|||
);
|
||||
}
|
||||
|
||||
getCredentials(
|
||||
provider: string,
|
||||
id: string,
|
||||
): Promise<APIKeyCredentials | OAuth2Credentials> {
|
||||
getCredentials(provider: string, id: string): Promise<Credentials> {
|
||||
return this._get(`/integrations/${provider}/credentials/${id}`);
|
||||
}
|
||||
|
||||
|
|
|
@ -97,7 +97,12 @@ export type BlockIOBooleanSubSchema = BlockIOSubSchemaMeta & {
|
|||
default?: boolean;
|
||||
};
|
||||
|
||||
export type CredentialsType = "api_key" | "oauth2";
|
||||
export type CredentialsType = "api_key" | "oauth2" | "user_password";
|
||||
|
||||
export type Credentials =
|
||||
| APIKeyCredentials
|
||||
| OAuth2Credentials
|
||||
| UserPasswordCredentials;
|
||||
|
||||
// --8<-- [start:BlockIOCredentialsSubSchema]
|
||||
export const PROVIDER_NAMES = {
|
||||
|
@ -105,10 +110,13 @@ export const PROVIDER_NAMES = {
|
|||
D_ID: "d_id",
|
||||
DISCORD: "discord",
|
||||
E2B: "e2b",
|
||||
EXA: "exa",
|
||||
FAL: "fal",
|
||||
GITHUB: "github",
|
||||
GOOGLE: "google",
|
||||
GOOGLE_MAPS: "google_maps",
|
||||
GROQ: "groq",
|
||||
HUBSPOT: "hubspot",
|
||||
IDEOGRAM: "ideogram",
|
||||
JINA: "jina",
|
||||
LINEAR: "linear",
|
||||
|
@ -121,13 +129,12 @@ export const PROVIDER_NAMES = {
|
|||
OPEN_ROUTER: "open_router",
|
||||
PINECONE: "pinecone",
|
||||
SLANT3D: "slant3d",
|
||||
SMTP: "smtp",
|
||||
TWITTER: "twitter",
|
||||
REPLICATE: "replicate",
|
||||
FAL: "fal",
|
||||
REDDIT: "reddit",
|
||||
REVID: "revid",
|
||||
UNREAL_SPEECH: "unreal_speech",
|
||||
EXA: "exa",
|
||||
HUBSPOT: "hubspot",
|
||||
TWITTER: "twitter",
|
||||
} as const;
|
||||
// --8<-- [end:BlockIOCredentialsSubSchema]
|
||||
|
||||
|
@ -323,8 +330,15 @@ export type APIKeyCredentials = BaseCredentials & {
|
|||
expires_at?: number;
|
||||
};
|
||||
|
||||
export type UserPasswordCredentials = BaseCredentials & {
|
||||
type: "user_password";
|
||||
title: string;
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
/* Mirror of backend/data/integrations.py:Webhook */
|
||||
type Webhook = {
|
||||
export type Webhook = {
|
||||
id: string;
|
||||
url: string;
|
||||
provider: CredentialsProviderName;
|
||||
|
|
|
@ -257,13 +257,13 @@ response = requests.post(
|
|||
)
|
||||
```
|
||||
|
||||
or use the shortcut `credentials.bearer()`:
|
||||
or use the shortcut `credentials.auth_header()`:
|
||||
|
||||
```python
|
||||
# credentials: APIKeyCredentials | OAuth2Credentials
|
||||
response = requests.post(
|
||||
url,
|
||||
headers={"Authorization": credentials.bearer()},
|
||||
headers={"Authorization": credentials.auth_header()},
|
||||
)
|
||||
```
|
||||
|
||||
|
|
Loading…
Reference in New Issue