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
Nicholas Tindle 2025-01-28 08:07:50 +00:00 committed by GitHub
parent 97ecaf5639
commit ac8a466cda
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 506 additions and 131 deletions

View File

@ -91,10 +91,12 @@ GROQ_API_KEY=
OPEN_ROUTER_API_KEY= OPEN_ROUTER_API_KEY=
# Reddit # 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_ID=
REDDIT_CLIENT_SECRET= REDDIT_CLIENT_SECRET=
REDDIT_USERNAME= REDDIT_USER_AGENT="AutoGPT:1.0 (by /u/autogpt)"
REDDIT_PASSWORD=
# Discord # Discord
DISCORD_BOT_TOKEN= DISCORD_BOT_TOKEN=

View File

@ -1,22 +1,53 @@
import smtplib import smtplib
from email.mime.multipart import MIMEMultipart from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText 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.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( 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_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): class SendEmailBlock(Block):
@ -30,10 +61,11 @@ class SendEmailBlock(Block):
body: str = SchemaField( body: str = SchemaField(
description="Body of the email", placeholder="Enter the email body" description="Body of the email", placeholder="Enter the email body"
) )
creds: EmailCredentials = SchemaField( config: SMTPConfig = SchemaField(
description="SMTP credentials", description="SMTP Config",
default=EmailCredentials(), default=SMTPConfig(),
) )
credentials: SMTPCredentialsInput = SMTPCredentialsField()
class Output(BlockSchema): class Output(BlockSchema):
status: str = SchemaField(description="Status of the email sending operation") status: str = SchemaField(description="Status of the email sending operation")
@ -43,7 +75,6 @@ class SendEmailBlock(Block):
def __init__(self): def __init__(self):
super().__init__( super().__init__(
disabled=True,
id="4335878a-394e-4e67-adf2-919877ff49ae", id="4335878a-394e-4e67-adf2-919877ff49ae",
description="This block sends an email using the provided SMTP credentials.", description="This block sends an email using the provided SMTP credentials.",
categories={BlockCategory.OUTPUT}, categories={BlockCategory.OUTPUT},
@ -53,25 +84,29 @@ class SendEmailBlock(Block):
"to_email": "recipient@example.com", "to_email": "recipient@example.com",
"subject": "Test Email", "subject": "Test Email",
"body": "This is a test email.", "body": "This is a test email.",
"creds": { "config": {
"smtp_server": "smtp.gmail.com", "smtp_server": "smtp.gmail.com",
"smtp_port": 25, "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_output=[("status", "Email sent successfully")],
test_mock={"send_email": lambda *args, **kwargs: "Email sent successfully"}, test_mock={"send_email": lambda *args, **kwargs: "Email sent successfully"},
) )
@staticmethod @staticmethod
def send_email( def send_email(
creds: EmailCredentials, to_email: str, subject: str, body: str config: SMTPConfig,
to_email: str,
subject: str,
body: str,
credentials: SMTPCredentials,
) -> str: ) -> str:
smtp_server = creds.smtp_server smtp_server = config.smtp_server
smtp_port = creds.smtp_port smtp_port = config.smtp_port
smtp_username = creds.smtp_username.get_secret_value() smtp_username = credentials.username.get_secret_value()
smtp_password = creds.smtp_password.get_secret_value() smtp_password = credentials.password.get_secret_value()
msg = MIMEMultipart() msg = MIMEMultipart()
msg["From"] = smtp_username msg["From"] = smtp_username
@ -86,10 +121,13 @@ class SendEmailBlock(Block):
return "Email sent successfully" 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( yield "status", self.send_email(
input_data.creds, config=input_data.config,
input_data.to_email, to_email=input_data.to_email,
input_data.subject, subject=input_data.subject,
input_data.body, body=input_data.body,
credentials=credentials,
) )

View File

@ -30,7 +30,7 @@ def _convert_to_api_url(url: str) -> str:
def _get_headers(credentials: GithubCredentials) -> dict[str, str]: def _get_headers(credentials: GithubCredentials) -> dict[str, str]:
return { return {
"Authorization": credentials.bearer(), "Authorization": credentials.auth_header(),
"Accept": "application/vnd.github.v3+json", "Accept": "application/vnd.github.v3+json",
} }

View File

@ -40,7 +40,7 @@ class LinearClient:
"Content-Type": "application/json", "Content-Type": "application/json",
} }
if credentials: if credentials:
headers["Authorization"] = credentials.bearer() headers["Authorization"] = credentials.auth_header()
self._requests = Requests( self._requests = Requests(
extra_headers=headers, extra_headers=headers,

View File

@ -1,22 +1,48 @@
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Iterator from typing import Iterator, Literal
import praw import praw
from pydantic import BaseModel, ConfigDict from pydantic import BaseModel, SecretStr
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema 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.mock import MockObject
from backend.util.settings import Settings
RedditCredentials = UserPasswordCredentials
RedditCredentialsInput = CredentialsMetaInput[
Literal[ProviderName.REDDIT],
Literal["user_password"],
]
class RedditCredentials(BaseModel): def RedditCredentialsField() -> RedditCredentialsInput:
client_id: BlockSecret = SecretField(key="reddit_client_id") """Creates a Reddit credentials input on a block."""
client_secret: BlockSecret = SecretField(key="reddit_client_secret") return CredentialsField(
username: BlockSecret = SecretField(key="reddit_username") description="The Reddit integration requires a username and password.",
password: BlockSecret = SecretField(key="reddit_password") )
user_agent: str = "AutoGPT:1.0 (by /u/autogpt)"
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): class RedditPost(BaseModel):
@ -31,13 +57,16 @@ class RedditComment(BaseModel):
comment: str comment: str
settings = Settings()
def get_praw(creds: RedditCredentials) -> praw.Reddit: def get_praw(creds: RedditCredentials) -> praw.Reddit:
client = praw.Reddit( client = praw.Reddit(
client_id=creds.client_id.get_secret_value(), client_id=settings.secrets.reddit_client_id,
client_secret=creds.client_secret.get_secret_value(), client_secret=settings.secrets.reddit_client_secret,
username=creds.username.get_secret_value(), username=creds.username.get_secret_value(),
password=creds.password.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() me = client.user.me()
if not me: if not me:
@ -48,11 +77,11 @@ def get_praw(creds: RedditCredentials) -> praw.Reddit:
class GetRedditPostsBlock(Block): class GetRedditPostsBlock(Block):
class Input(BlockSchema): class Input(BlockSchema):
subreddit: str = SchemaField(description="Subreddit name") subreddit: str = SchemaField(
creds: RedditCredentials = SchemaField( description="Subreddit name, excluding the /r/ prefix",
description="Reddit credentials", default="writingprompts",
default=RedditCredentials(),
) )
credentials: RedditCredentialsInput = RedditCredentialsField()
last_minutes: int | None = SchemaField( last_minutes: int | None = SchemaField(
description="Post time to stop minutes ago while fetching posts", description="Post time to stop minutes ago while fetching posts",
default=None, default=None,
@ -70,20 +99,18 @@ class GetRedditPostsBlock(Block):
def __init__(self): def __init__(self):
super().__init__( super().__init__(
disabled=True,
id="c6731acb-4285-4ee1-bc9b-03d0766c370f", id="c6731acb-4285-4ee1-bc9b-03d0766c370f",
description="This block fetches Reddit posts from a defined subreddit name.", description="This block fetches Reddit posts from a defined subreddit name.",
categories={BlockCategory.SOCIAL}, categories={BlockCategory.SOCIAL},
disabled=(
not settings.secrets.reddit_client_id
or not settings.secrets.reddit_client_secret
),
input_schema=GetRedditPostsBlock.Input, input_schema=GetRedditPostsBlock.Input,
output_schema=GetRedditPostsBlock.Output, output_schema=GetRedditPostsBlock.Output,
test_credentials=TEST_CREDENTIALS,
test_input={ test_input={
"creds": { "credentials": TEST_CREDENTIALS_INPUT,
"client_id": "client_id",
"client_secret": "client_secret",
"username": "username",
"password": "password",
"user_agent": "user_agent",
},
"subreddit": "subreddit", "subreddit": "subreddit",
"last_post": "id3", "last_post": "id3",
"post_limit": 2, "post_limit": 2,
@ -103,7 +130,7 @@ class GetRedditPostsBlock(Block):
), ),
], ],
test_mock={ test_mock={
"get_posts": lambda _: [ "get_posts": lambda input_data, credentials: [
MockObject(id="id1", title="title1", selftext="body1"), MockObject(id="id1", title="title1", selftext="body1"),
MockObject(id="id2", title="title2", selftext="body2"), MockObject(id="id2", title="title2", selftext="body2"),
MockObject(id="id3", title="title2", selftext="body2"), MockObject(id="id3", title="title2", selftext="body2"),
@ -112,14 +139,18 @@ class GetRedditPostsBlock(Block):
) )
@staticmethod @staticmethod
def get_posts(input_data: Input) -> Iterator[praw.reddit.Submission]: def get_posts(
client = get_praw(input_data.creds) input_data: Input, *, credentials: RedditCredentials
) -> Iterator[praw.reddit.Submission]:
client = get_praw(credentials)
subreddit = client.subreddit(input_data.subreddit) subreddit = client.subreddit(input_data.subreddit)
return subreddit.new(limit=input_data.post_limit or 10) 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) 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: if input_data.last_minutes:
post_datetime = datetime.fromtimestamp( post_datetime = datetime.fromtimestamp(
post.created_utc, tz=timezone.utc post.created_utc, tz=timezone.utc
@ -141,9 +172,7 @@ class GetRedditPostsBlock(Block):
class PostRedditCommentBlock(Block): class PostRedditCommentBlock(Block):
class Input(BlockSchema): class Input(BlockSchema):
creds: RedditCredentials = SchemaField( credentials: RedditCredentialsInput = RedditCredentialsField()
description="Reddit credentials", default=RedditCredentials()
)
data: RedditComment = SchemaField(description="Reddit comment") data: RedditComment = SchemaField(description="Reddit comment")
class Output(BlockSchema): class Output(BlockSchema):
@ -156,7 +185,15 @@ class PostRedditCommentBlock(Block):
categories={BlockCategory.SOCIAL}, categories={BlockCategory.SOCIAL},
input_schema=PostRedditCommentBlock.Input, input_schema=PostRedditCommentBlock.Input,
output_schema=PostRedditCommentBlock.Output, 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_output=[("comment_id", "dummy_comment_id")],
test_mock={"reply_post": lambda creds, comment: "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.") raise ValueError("Failed to post comment.")
return new_comment.id return new_comment.id
def run(self, input_data: Input, **kwargs) -> BlockOutput: def run(
yield "comment_id", self.reply_post(input_data.creds, input_data.data) self, input_data: Input, *, credentials: RedditCredentials, **kwargs
) -> BlockOutput:
yield "comment_id", self.reply_post(credentials, input_data.data)

View File

@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
import base64
import logging import logging
from typing import ( from typing import (
TYPE_CHECKING, TYPE_CHECKING,
@ -199,27 +200,42 @@ class OAuth2Credentials(_BaseCredentials):
scopes: list[str] scopes: list[str]
metadata: dict[str, Any] = Field(default_factory=dict) 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()}" return f"Bearer {self.access_token.get_secret_value()}"
class APIKeyCredentials(_BaseCredentials): class APIKeyCredentials(_BaseCredentials):
type: Literal["api_key"] = "api_key" type: Literal["api_key"] = "api_key"
api_key: SecretStr 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)""" """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()}" 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[ Credentials = Annotated[
OAuth2Credentials | APIKeyCredentials, OAuth2Credentials | APIKeyCredentials | UserPasswordCredentials,
Field(discriminator="type"), Field(discriminator="type"),
] ]
CredentialsType = Literal["api_key", "oauth2"] CredentialsType = Literal["api_key", "oauth2", "user_password"]
class OAuthState(BaseModel): class OAuthState(BaseModel):

View File

@ -26,9 +26,11 @@ class ProviderName(str, Enum):
OPENWEATHERMAP = "openweathermap" OPENWEATHERMAP = "openweathermap"
OPEN_ROUTER = "open_router" OPEN_ROUTER = "open_router"
PINECONE = "pinecone" PINECONE = "pinecone"
REDDIT = "reddit"
REPLICATE = "replicate" REPLICATE = "replicate"
REVID = "revid" REVID = "revid"
SLANT3D = "slant3d" SLANT3D = "slant3d"
SMTP = "smtp"
TWITTER = "twitter" TWITTER = "twitter"
UNREAL_SPEECH = "unreal_speech" UNREAL_SPEECH = "unreal_speech"
# --8<-- [end:ProviderName] # --8<-- [end:ProviderName]

View File

@ -168,7 +168,7 @@ class BaseWebhooksManager(ABC, Generic[WT]):
id = str(uuid4()) id = str(uuid4())
secret = secrets.token_hex(32) 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) ingress_url = webhook_ingress_url(provider_name=provider_name, webhook_id=id)
if register: if register:
if not credentials: if not credentials:

View File

@ -1,7 +1,7 @@
import logging import logging
from backend.data import integrations from backend.data import integrations
from backend.data.model import APIKeyCredentials, Credentials, OAuth2Credentials from backend.data.model import Credentials
from ._base import WT, BaseWebhooksManager from ._base import WT, BaseWebhooksManager
@ -25,6 +25,6 @@ class ManualWebhookManagerBase(BaseWebhooksManager[WT]):
async def _deregister_webhook( async def _deregister_webhook(
self, self,
webhook: integrations.Webhook, webhook: integrations.Webhook,
credentials: OAuth2Credentials | APIKeyCredentials, credentials: Credentials,
) -> None: ) -> None:
pass pass

View File

@ -67,7 +67,7 @@ class GithubWebhooksManager(BaseWebhooksManager):
headers = { headers = {
**self.GITHUB_API_DEFAULT_HEADERS, **self.GITHUB_API_DEFAULT_HEADERS,
"Authorization": credentials.bearer(), "Authorization": credentials.auth_header(),
} }
repo, github_hook_id = webhook.resource, webhook.provider_webhook_id repo, github_hook_id = webhook.resource, webhook.provider_webhook_id
@ -96,7 +96,7 @@ class GithubWebhooksManager(BaseWebhooksManager):
headers = { headers = {
**self.GITHUB_API_DEFAULT_HEADERS, **self.GITHUB_API_DEFAULT_HEADERS,
"Authorization": credentials.bearer(), "Authorization": credentials.auth_header(),
} }
webhook_data = { webhook_data = {
"name": "web", "name": "web",
@ -142,7 +142,7 @@ class GithubWebhooksManager(BaseWebhooksManager):
headers = { headers = {
**self.GITHUB_API_DEFAULT_HEADERS, **self.GITHUB_API_DEFAULT_HEADERS,
"Authorization": credentials.bearer(), "Authorization": credentials.auth_header(),
} }
if webhook_type == self.WebhookType.REPO: if webhook_type == self.WebhookType.REPO:

View File

@ -2,7 +2,7 @@ import logging
from typing import TYPE_CHECKING, Annotated, Literal from typing import TYPE_CHECKING, Annotated, Literal
from fastapi import APIRouter, Body, Depends, HTTPException, Path, Query, Request 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.graph import set_node_webhook
from backend.data.integrations import ( from backend.data.integrations import (
@ -12,12 +12,7 @@ from backend.data.integrations import (
publish_webhook_event, publish_webhook_event,
wait_for_webhook_event, wait_for_webhook_event,
) )
from backend.data.model import ( from backend.data.model import Credentials, CredentialsType, OAuth2Credentials
APIKeyCredentials,
Credentials,
CredentialsType,
OAuth2Credentials,
)
from backend.executor.manager import ExecutionManager from backend.executor.manager import ExecutionManager
from backend.integrations.creds_manager import IntegrationCredentialsManager from backend.integrations.creds_manager import IntegrationCredentialsManager
from backend.integrations.oauth import HANDLERS_BY_NAME from backend.integrations.oauth import HANDLERS_BY_NAME
@ -204,31 +199,21 @@ def get_credential(
@router.post("/{provider}/credentials", status_code=201) @router.post("/{provider}/credentials", status_code=201)
def create_api_key_credentials( def create_credentials(
user_id: Annotated[str, Depends(get_user_id)], user_id: Annotated[str, Depends(get_user_id)],
provider: Annotated[ provider: Annotated[
ProviderName, Path(title="The provider to create credentials for") ProviderName, Path(title="The provider to create credentials for")
], ],
api_key: Annotated[str, Body(title="The API key to store")], credentials: Credentials,
title: Annotated[str, Body(title="Optional title for the credentials")], ) -> Credentials:
expires_at: Annotated[ credentials.provider = provider
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,
)
try: try:
creds_manager.create(user_id, new_credentials) creds_manager.create(user_id, credentials)
except Exception as e: except Exception as e:
raise HTTPException( raise HTTPException(
status_code=500, detail=f"Failed to store credentials: {str(e)}" status_code=500, detail=f"Failed to store credentials: {str(e)}"
) )
return new_credentials return credentials
class CredentialsDeletionResponse(BaseModel): class CredentialsDeletionResponse(BaseModel):

View File

@ -157,6 +157,11 @@ class Config(UpdateTrackingModel["Config"], BaseSettings):
description="The name of the Google Cloud Storage bucket for media files", 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( scheduler_db_pool_size: int = Field(
default=3, default=3,
description="The pool size for the scheduler database connection pool", 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_id: str = Field(default="", description="Reddit client ID")
reddit_client_secret: str = Field(default="", description="Reddit client secret") 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( openweathermap_api_key: str = Field(
default="", description="OpenWeatherMap API key" default="", description="OpenWeatherMap API key"

View File

@ -124,14 +124,22 @@ export default function PrivatePage() {
const allCredentials = providers const allCredentials = providers
? Object.values(providers).flatMap((provider) => ? Object.values(providers).flatMap((provider) =>
[...provider.savedOAuthCredentials, ...provider.savedApiKeys] [
...provider.savedOAuthCredentials,
...provider.savedApiKeys,
...provider.savedUserPasswordCredentials,
]
.filter((cred) => !hiddenCredentials.includes(cred.id)) .filter((cred) => !hiddenCredentials.includes(cred.id))
.map((credentials) => ({ .map((credentials) => ({
...credentials, ...credentials,
provider: provider.provider, provider: provider.provider,
providerName: provider.providerName, providerName: provider.providerName,
ProviderIcon: providerIcons[provider.provider], ProviderIcon: providerIcons[provider.provider],
TypeIcon: { oauth2: IconUser, api_key: IconKey }[credentials.type], TypeIcon: {
oauth2: IconUser,
api_key: IconKey,
user_password: IconKey,
}[credentials.type],
})), })),
) )
: []; : [];

View File

@ -124,14 +124,22 @@ export default function PrivatePage() {
const allCredentials = providers const allCredentials = providers
? Object.values(providers).flatMap((provider) => ? Object.values(providers).flatMap((provider) =>
[...provider.savedOAuthCredentials, ...provider.savedApiKeys] [
...provider.savedOAuthCredentials,
...provider.savedApiKeys,
...provider.savedUserPasswordCredentials,
]
.filter((cred) => !hiddenCredentials.includes(cred.id)) .filter((cred) => !hiddenCredentials.includes(cred.id))
.map((credentials) => ({ .map((credentials) => ({
...credentials, ...credentials,
provider: provider.provider, provider: provider.provider,
providerName: provider.providerName, providerName: provider.providerName,
ProviderIcon: providerIcons[provider.provider], 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", oauth2: "OAuth2 credentials",
api_key: "API key", api_key: "API key",
user_password: "User password",
}[cred.type] }[cred.type]
}{" "} }{" "}
- <code>{cred.id}</code> - <code>{cred.id}</code>

View File

@ -75,7 +75,9 @@ export const providerIcons: Record<
open_router: fallbackIcon, open_router: fallbackIcon,
pinecone: fallbackIcon, pinecone: fallbackIcon,
slant3d: fallbackIcon, slant3d: fallbackIcon,
smtp: fallbackIcon,
replicate: fallbackIcon, replicate: fallbackIcon,
reddit: fallbackIcon,
fal: fallbackIcon, fal: fallbackIcon,
revid: fallbackIcon, revid: fallbackIcon,
twitter: FaTwitter, twitter: FaTwitter,
@ -107,6 +109,10 @@ export const CredentialsInput: FC<{
const credentials = useCredentials(selfKey); const credentials = useCredentials(selfKey);
const [isAPICredentialsModalOpen, setAPICredentialsModalOpen] = const [isAPICredentialsModalOpen, setAPICredentialsModalOpen] =
useState(false); useState(false);
const [
isUserPasswordCredentialsModalOpen,
setUserPasswordCredentialsModalOpen,
] = useState(false);
const [isOAuth2FlowInProgress, setOAuth2FlowInProgress] = useState(false); const [isOAuth2FlowInProgress, setOAuth2FlowInProgress] = useState(false);
const [oAuthPopupController, setOAuthPopupController] = const [oAuthPopupController, setOAuthPopupController] =
useState<AbortController | null>(null); useState<AbortController | null>(null);
@ -122,8 +128,10 @@ export const CredentialsInput: FC<{
providerName, providerName,
supportsApiKey, supportsApiKey,
supportsOAuth2, supportsOAuth2,
supportsUserPassword,
savedApiKeys, savedApiKeys,
savedOAuthCredentials, savedOAuthCredentials,
savedUserPasswordCredentials,
oAuthCallback, oAuthCallback,
} = credentials; } = credentials;
@ -237,6 +245,17 @@ export const CredentialsInput: FC<{
providerName={providerName} 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 && selectedCredentials &&
!savedApiKeys !savedApiKeys
.concat(savedOAuthCredentials) .concat(savedOAuthCredentials)
.concat(savedUserPasswordCredentials)
.some((c) => c.id === selectedCredentials.id) .some((c) => c.id === selectedCredentials.id)
) { ) {
onSelectCredentials(undefined); onSelectCredentials(undefined);
} }
// No saved credentials yet // No saved credentials yet
if (savedApiKeys.length === 0 && savedOAuthCredentials.length === 0) { if (
savedApiKeys.length === 0 &&
savedOAuthCredentials.length === 0 &&
savedUserPasswordCredentials.length === 0
) {
return ( return (
<> <>
<div className="mb-2 flex gap-1"> <div className="mb-2 flex gap-1">
@ -273,6 +297,12 @@ export const CredentialsInput: FC<{
Enter API key Enter API key
</Button> </Button>
)} )}
{supportsUserPassword && (
<Button onClick={() => setUserPasswordCredentialsModalOpen(true)}>
<ProviderIcon className="mr-2 h-4 w-4" />
Enter username and password
</Button>
)}
</div> </div>
{modals} {modals}
{oAuthError && ( {oAuthError && (
@ -282,12 +312,29 @@ export const CredentialsInput: FC<{
); );
} }
const singleCredential = const getCredentialCounts = () => ({
savedApiKeys.length === 1 && savedOAuthCredentials.length === 0 apiKeys: savedApiKeys.length,
? savedApiKeys[0] oauth: savedOAuthCredentials.length,
: savedOAuthCredentials.length === 1 && savedApiKeys.length === 0 userPass: savedUserPasswordCredentials.length,
? savedOAuthCredentials[0] });
: null;
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 (singleCredential) {
if (!selectedCredentials) { if (!selectedCredentials) {
@ -311,6 +358,7 @@ export const CredentialsInput: FC<{
} else { } else {
const selectedCreds = savedApiKeys const selectedCreds = savedApiKeys
.concat(savedOAuthCredentials) .concat(savedOAuthCredentials)
.concat(savedUserPasswordCredentials)
.find((c) => c.id == newValue)!; .find((c) => c.id == newValue)!;
onSelectCredentials({ onSelectCredentials({
@ -349,6 +397,13 @@ export const CredentialsInput: FC<{
{credentials.title} {credentials.title}
</SelectItem> </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 /> <SelectSeparator />
{supportsOAuth2 && ( {supportsOAuth2 && (
<SelectItem value="sign-in"> <SelectItem value="sign-in">
@ -362,6 +417,12 @@ export const CredentialsInput: FC<{
Add new API key Add new API key
</SelectItem> </SelectItem>
)} )}
{supportsUserPassword && (
<SelectItem value="add-user-password">
<IconUserPlus className="mr-1.5 inline" />
Add new user password
</SelectItem>
)}
</SelectContent> </SelectContent>
</Select> </Select>
{modals} {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<{ export const OAuth2FlowWaitingModal: FC<{
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;

View File

@ -5,6 +5,7 @@ import {
CredentialsMetaResponse, CredentialsMetaResponse,
CredentialsProviderName, CredentialsProviderName,
PROVIDER_NAMES, PROVIDER_NAMES,
UserPasswordCredentials,
} from "@/lib/autogpt-server-api"; } from "@/lib/autogpt-server-api";
import { useBackendAPI } from "@/lib/autogpt-server-api/context"; import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import { createContext, useCallback, useEffect, useState } from "react"; import { createContext, useCallback, useEffect, useState } from "react";
@ -20,10 +21,13 @@ const providerDisplayNames: Record<CredentialsProviderName, string> = {
discord: "Discord", discord: "Discord",
d_id: "D-ID", d_id: "D-ID",
e2b: "E2B", e2b: "E2B",
exa: "Exa",
fal: "FAL",
github: "GitHub", github: "GitHub",
google: "Google", google: "Google",
google_maps: "Google Maps", google_maps: "Google Maps",
groq: "Groq", groq: "Groq",
hubspot: "Hubspot",
ideogram: "Ideogram", ideogram: "Ideogram",
jina: "Jina", jina: "Jina",
linear: "Linear", linear: "Linear",
@ -36,13 +40,12 @@ const providerDisplayNames: Record<CredentialsProviderName, string> = {
open_router: "Open Router", open_router: "Open Router",
pinecone: "Pinecone", pinecone: "Pinecone",
slant3d: "Slant3D", slant3d: "Slant3D",
smtp: "SMTP",
reddit: "Reddit",
replicate: "Replicate", replicate: "Replicate",
fal: "FAL",
revid: "Rev.ID", revid: "Rev.ID",
twitter: "Twitter", twitter: "Twitter",
unreal_speech: "Unreal Speech", unreal_speech: "Unreal Speech",
exa: "Exa",
hubspot: "Hubspot",
} as const; } as const;
// --8<-- [end:CredentialsProviderNames] // --8<-- [end:CredentialsProviderNames]
@ -51,11 +54,17 @@ type APIKeyCredentialsCreatable = Omit<
"id" | "provider" | "type" "id" | "provider" | "type"
>; >;
type UserPasswordCredentialsCreatable = Omit<
UserPasswordCredentials,
"id" | "provider" | "type"
>;
export type CredentialsProviderData = { export type CredentialsProviderData = {
provider: CredentialsProviderName; provider: CredentialsProviderName;
providerName: string; providerName: string;
savedApiKeys: CredentialsMetaResponse[]; savedApiKeys: CredentialsMetaResponse[];
savedOAuthCredentials: CredentialsMetaResponse[]; savedOAuthCredentials: CredentialsMetaResponse[];
savedUserPasswordCredentials: CredentialsMetaResponse[];
oAuthCallback: ( oAuthCallback: (
code: string, code: string,
state_token: string, state_token: string,
@ -63,6 +72,9 @@ export type CredentialsProviderData = {
createAPIKeyCredentials: ( createAPIKeyCredentials: (
credentials: APIKeyCredentialsCreatable, credentials: APIKeyCredentialsCreatable,
) => Promise<CredentialsMetaResponse>; ) => Promise<CredentialsMetaResponse>;
createUserPasswordCredentials: (
credentials: UserPasswordCredentialsCreatable,
) => Promise<CredentialsMetaResponse>;
deleteCredentials: ( deleteCredentials: (
id: string, id: string,
force?: boolean, force?: boolean,
@ -107,6 +119,11 @@ export default function CredentialsProvider({
...updatedProvider.savedOAuthCredentials, ...updatedProvider.savedOAuthCredentials,
credentials, credentials,
]; ];
} else if (credentials.type === "user_password") {
updatedProvider.savedUserPasswordCredentials = [
...updatedProvider.savedUserPasswordCredentials,
credentials,
];
} }
return { return {
@ -148,6 +165,22 @@ export default function CredentialsProvider({
[api, addCredentials], [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. */ /** Wraps `BackendAPI.deleteCredentials`, and removes the credentials from the internal store. */
const deleteCredentials = useCallback( const deleteCredentials = useCallback(
async ( async (
@ -172,7 +205,10 @@ export default function CredentialsProvider({
updatedProvider.savedOAuthCredentials.filter( updatedProvider.savedOAuthCredentials.filter(
(cred) => cred.id !== id, (cred) => cred.id !== id,
); );
updatedProvider.savedUserPasswordCredentials =
updatedProvider.savedUserPasswordCredentials.filter(
(cred) => cred.id !== id,
);
return { return {
...prev, ...prev,
[provider]: updatedProvider, [provider]: updatedProvider,
@ -191,12 +227,18 @@ export default function CredentialsProvider({
const credentialsByProvider = response.reduce( const credentialsByProvider = response.reduce(
(acc, cred) => { (acc, cred) => {
if (!acc[cred.provider]) { if (!acc[cred.provider]) {
acc[cred.provider] = { oauthCreds: [], apiKeys: [] }; acc[cred.provider] = {
oauthCreds: [],
apiKeys: [],
userPasswordCreds: [],
};
} }
if (cred.type === "oauth2") { if (cred.type === "oauth2") {
acc[cred.provider].oauthCreds.push(cred); acc[cred.provider].oauthCreds.push(cred);
} else if (cred.type === "api_key") { } else if (cred.type === "api_key") {
acc[cred.provider].apiKeys.push(cred); acc[cred.provider].apiKeys.push(cred);
} else if (cred.type === "user_password") {
acc[cred.provider].userPasswordCreds.push(cred);
} }
return acc; return acc;
}, },
@ -205,6 +247,7 @@ export default function CredentialsProvider({
{ {
oauthCreds: CredentialsMetaResponse[]; oauthCreds: CredentialsMetaResponse[];
apiKeys: CredentialsMetaResponse[]; apiKeys: CredentialsMetaResponse[];
userPasswordCreds: CredentialsMetaResponse[];
} }
>, >,
); );
@ -221,6 +264,8 @@ export default function CredentialsProvider({
savedApiKeys: credentialsByProvider[provider]?.apiKeys ?? [], savedApiKeys: credentialsByProvider[provider]?.apiKeys ?? [],
savedOAuthCredentials: savedOAuthCredentials:
credentialsByProvider[provider]?.oauthCreds ?? [], credentialsByProvider[provider]?.oauthCreds ?? [],
savedUserPasswordCredentials:
credentialsByProvider[provider]?.userPasswordCreds ?? [],
oAuthCallback: (code: string, state_token: string) => oAuthCallback: (code: string, state_token: string) =>
oAuthCallback( oAuthCallback(
provider as CredentialsProviderName, provider as CredentialsProviderName,
@ -234,6 +279,13 @@ export default function CredentialsProvider({
provider as CredentialsProviderName, provider as CredentialsProviderName,
credentials, credentials,
), ),
createUserPasswordCredentials: (
credentials: UserPasswordCredentialsCreatable,
) =>
createUserPasswordCredentials(
provider as CredentialsProviderName,
credentials,
),
deleteCredentials: (id: string, force: boolean = false) => deleteCredentials: (id: string, force: boolean = false) =>
deleteCredentials( deleteCredentials(
provider as CredentialsProviderName, provider as CredentialsProviderName,
@ -246,7 +298,13 @@ export default function CredentialsProvider({
})); }));
}); });
}); });
}, [api, createAPIKeyCredentials, deleteCredentials, oAuthCallback]); }, [
api,
createAPIKeyCredentials,
createUserPasswordCredentials,
deleteCredentials,
oAuthCallback,
]);
return ( return (
<CredentialsProvidersContext.Provider value={providers}> <CredentialsProvidersContext.Provider value={providers}>

View File

@ -17,12 +17,14 @@ export type CredentialsData =
schema: BlockIOCredentialsSubSchema; schema: BlockIOCredentialsSubSchema;
supportsApiKey: boolean; supportsApiKey: boolean;
supportsOAuth2: boolean; supportsOAuth2: boolean;
supportsUserPassword: boolean;
isLoading: true; isLoading: true;
} }
| (CredentialsProviderData & { | (CredentialsProviderData & {
schema: BlockIOCredentialsSubSchema; schema: BlockIOCredentialsSubSchema;
supportsApiKey: boolean; supportsApiKey: boolean;
supportsOAuth2: boolean; supportsOAuth2: boolean;
supportsUserPassword: boolean;
isLoading: false; isLoading: false;
}); });
@ -72,6 +74,8 @@ export default function useCredentials(
const supportsApiKey = const supportsApiKey =
credentialsSchema.credentials_types.includes("api_key"); credentialsSchema.credentials_types.includes("api_key");
const supportsOAuth2 = credentialsSchema.credentials_types.includes("oauth2"); const supportsOAuth2 = credentialsSchema.credentials_types.includes("oauth2");
const supportsUserPassword =
credentialsSchema.credentials_types.includes("user_password");
// No provider means maybe it's still loading // No provider means maybe it's still loading
if (!provider) { if (!provider) {
@ -93,13 +97,17 @@ export default function useCredentials(
) )
: provider.savedOAuthCredentials; : provider.savedOAuthCredentials;
const savedUserPasswordCredentials = provider.savedUserPasswordCredentials;
return { return {
...provider, ...provider,
provider: providerName, provider: providerName,
schema: credentialsSchema, schema: credentialsSchema,
supportsApiKey, supportsApiKey,
supportsOAuth2, supportsOAuth2,
supportsUserPassword,
savedOAuthCredentials, savedOAuthCredentials,
savedUserPasswordCredentials,
isLoading: false, isLoading: false,
}; };
} }

View File

@ -15,7 +15,6 @@ import {
GraphUpdateable, GraphUpdateable,
NodeExecutionResult, NodeExecutionResult,
MyAgentsResponse, MyAgentsResponse,
OAuth2Credentials,
ProfileDetails, ProfileDetails,
User, User,
StoreAgentsResponse, StoreAgentsResponse,
@ -29,6 +28,8 @@ import {
StoreReview, StoreReview,
ScheduleCreatable, ScheduleCreatable,
Schedule, Schedule,
UserPasswordCredentials,
Credentials,
APIKeyPermission, APIKeyPermission,
CreateAPIKeyResponse, CreateAPIKeyResponse,
APIKey, APIKey,
@ -203,7 +204,17 @@ export default class BackendAPI {
return this._request( return this._request(
"POST", "POST",
`/integrations/${credentials.provider}/credentials`, `/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( getCredentials(provider: string, id: string): Promise<Credentials> {
provider: string,
id: string,
): Promise<APIKeyCredentials | OAuth2Credentials> {
return this._get(`/integrations/${provider}/credentials/${id}`); return this._get(`/integrations/${provider}/credentials/${id}`);
} }

View File

@ -97,7 +97,12 @@ export type BlockIOBooleanSubSchema = BlockIOSubSchemaMeta & {
default?: boolean; 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] // --8<-- [start:BlockIOCredentialsSubSchema]
export const PROVIDER_NAMES = { export const PROVIDER_NAMES = {
@ -105,10 +110,13 @@ export const PROVIDER_NAMES = {
D_ID: "d_id", D_ID: "d_id",
DISCORD: "discord", DISCORD: "discord",
E2B: "e2b", E2B: "e2b",
EXA: "exa",
FAL: "fal",
GITHUB: "github", GITHUB: "github",
GOOGLE: "google", GOOGLE: "google",
GOOGLE_MAPS: "google_maps", GOOGLE_MAPS: "google_maps",
GROQ: "groq", GROQ: "groq",
HUBSPOT: "hubspot",
IDEOGRAM: "ideogram", IDEOGRAM: "ideogram",
JINA: "jina", JINA: "jina",
LINEAR: "linear", LINEAR: "linear",
@ -121,13 +129,12 @@ export const PROVIDER_NAMES = {
OPEN_ROUTER: "open_router", OPEN_ROUTER: "open_router",
PINECONE: "pinecone", PINECONE: "pinecone",
SLANT3D: "slant3d", SLANT3D: "slant3d",
SMTP: "smtp",
TWITTER: "twitter",
REPLICATE: "replicate", REPLICATE: "replicate",
FAL: "fal", REDDIT: "reddit",
REVID: "revid", REVID: "revid",
UNREAL_SPEECH: "unreal_speech", UNREAL_SPEECH: "unreal_speech",
EXA: "exa",
HUBSPOT: "hubspot",
TWITTER: "twitter",
} as const; } as const;
// --8<-- [end:BlockIOCredentialsSubSchema] // --8<-- [end:BlockIOCredentialsSubSchema]
@ -323,8 +330,15 @@ export type APIKeyCredentials = BaseCredentials & {
expires_at?: number; expires_at?: number;
}; };
export type UserPasswordCredentials = BaseCredentials & {
type: "user_password";
title: string;
username: string;
password: string;
};
/* Mirror of backend/data/integrations.py:Webhook */ /* Mirror of backend/data/integrations.py:Webhook */
type Webhook = { export type Webhook = {
id: string; id: string;
url: string; url: string;
provider: CredentialsProviderName; provider: CredentialsProviderName;

View File

@ -257,13 +257,13 @@ response = requests.post(
) )
``` ```
or use the shortcut `credentials.bearer()`: or use the shortcut `credentials.auth_header()`:
```python ```python
# credentials: APIKeyCredentials | OAuth2Credentials # credentials: APIKeyCredentials | OAuth2Credentials
response = requests.post( response = requests.post(
url, url,
headers={"Authorization": credentials.bearer()}, headers={"Authorization": credentials.auth_header()},
) )
``` ```