feat(backend, frontend): make changes to use our security modules more effectively (#10123)

<!-- Clearly explain the need for these changes: -->
Doing the CASA Audit and this is something to check
### Changes 🏗️
- limits APIs to use their specific endpoints
- use expected trusted sources for each block and requests call
- Use cryptographically valid string comparisons
- Don't log secrets

<!-- Concisely describe all of the changes made in this pull request:
-->

### 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] Testing in dev branch once merged

---------

Co-authored-by: Swifty <craigswift13@gmail.com>
This commit is contained in:
Nicholas Tindle
2025-06-16 10:22:08 -05:00
committed by GitHub
parent f950f35af8
commit 81d3eb7c34
36 changed files with 312 additions and 102 deletions

1
.gitignore vendored
View File

@@ -176,3 +176,4 @@ autogpt_platform/backend/settings.py
*.ign.* *.ign.*
.test-contents .test-contents
.claude/settings.local.json

View File

@@ -31,4 +31,5 @@ class APIKeyManager:
"""Verify if a provided API key matches the stored hash.""" """Verify if a provided API key matches the stored hash."""
if not provided_key.startswith(self.PREFIX): if not provided_key.startswith(self.PREFIX):
return False return False
return hashlib.sha256(provided_key.encode()).hexdigest() == stored_hash provided_hash = hashlib.sha256(provided_key.encode()).hexdigest()
return secrets.compare_digest(provided_hash, stored_hash)

View File

@@ -1,5 +1,6 @@
import inspect import inspect
import logging import logging
import secrets
from typing import Any, Callable, Optional from typing import Any, Callable, Optional
from fastapi import HTTPException, Request, Security from fastapi import HTTPException, Request, Security
@@ -93,7 +94,11 @@ class APIKeyValidator:
self.error_message = error_message self.error_message = error_message
async def default_validator(self, api_key: str) -> bool: async def default_validator(self, api_key: str) -> bool:
return api_key == self.expected_token if not self.expected_token:
raise ValueError(
"Expected Token Required to be set when uisng API Key Validator default validation"
)
return secrets.compare_digest(api_key, self.expected_token)
async def __call__( async def __call__(
self, request: Request, api_key: str = Security(APIKeyHeader) self, request: Request, api_key: str = Security(APIKeyHeader)

View File

@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. # This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand.
[[package]] [[package]]
name = "aiohappyeyeballs" name = "aiohappyeyeballs"
@@ -177,7 +177,7 @@ files = [
{file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"},
{file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"},
] ]
markers = {main = "python_version < \"3.11\"", dev = "python_full_version < \"3.11.3\""} markers = {main = "python_version == \"3.10\"", dev = "python_full_version < \"3.11.3\""}
[[package]] [[package]]
name = "attrs" name = "attrs"
@@ -323,6 +323,21 @@ files = [
{file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"},
] ]
[[package]]
name = "click"
version = "8.2.1"
description = "Composable command line interface toolkit"
optional = false
python-versions = ">=3.10"
groups = ["main"]
files = [
{file = "click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b"},
{file = "click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202"},
]
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
[[package]] [[package]]
name = "colorama" name = "colorama"
version = "0.4.6" version = "0.4.6"
@@ -375,7 +390,7 @@ description = "Backport of PEP 654 (exception groups)"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
groups = ["main"] groups = ["main"]
markers = "python_version < \"3.11\"" markers = "python_version == \"3.10\""
files = [ files = [
{file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"},
{file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"},
@@ -399,6 +414,27 @@ files = [
[package.extras] [package.extras]
tests = ["coverage", "coveralls", "dill", "mock", "nose"] tests = ["coverage", "coveralls", "dill", "mock", "nose"]
[[package]]
name = "fastapi"
version = "0.115.12"
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "fastapi-0.115.12-py3-none-any.whl", hash = "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d"},
{file = "fastapi-0.115.12.tar.gz", hash = "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681"},
]
[package.dependencies]
pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0"
starlette = ">=0.40.0,<0.47.0"
typing-extensions = ">=4.8.0"
[package.extras]
all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"]
[[package]] [[package]]
name = "frozenlist" name = "frozenlist"
version = "1.4.1" version = "1.4.1"
@@ -895,6 +931,47 @@ files = [
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
] ]
[[package]]
name = "launchdarkly-eventsource"
version = "1.2.4"
description = "LaunchDarkly SSE Client"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "launchdarkly_eventsource-1.2.4-py3-none-any.whl", hash = "sha256:048ef8c4440d0d8219778661ee4d4b5e12aa6ed2c29a3004417ede44c2386e8c"},
{file = "launchdarkly_eventsource-1.2.4.tar.gz", hash = "sha256:b8b9342681f55e1d35c56243431cbbaca4eb9812d6785f8de204af322104e066"},
]
[package.dependencies]
urllib3 = ">=1.26.0,<3"
[[package]]
name = "launchdarkly-server-sdk"
version = "9.11.1"
description = "LaunchDarkly SDK for Python"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "launchdarkly_server_sdk-9.11.1-py3-none-any.whl", hash = "sha256:128569cebf666dd115cc0ba03c48ff75f6acc9788301a7e2c3a54d06107e445a"},
{file = "launchdarkly_server_sdk-9.11.1.tar.gz", hash = "sha256:150e29656cb8c506d1967f3c59e62b69310d345ec27217640a6146dd1db5d250"},
]
[package.dependencies]
certifi = ">=2018.4.16"
expiringdict = ">=1.1.4"
launchdarkly-eventsource = ">=1.2.4,<2.0.0"
pyRFC3339 = ">=1.0"
semver = ">=2.10.2"
urllib3 = ">=1.26.0,<3"
[package.extras]
consul = ["python-consul (>=1.0.1)"]
dynamodb = ["boto3 (>=1.9.71)"]
redis = ["redis (>=2.10.5)"]
test-filesource = ["pyyaml (>=5.3.1)", "watchdog (>=3.0.0)"]
[[package]] [[package]]
name = "multidict" name = "multidict"
version = "6.1.0" version = "6.1.0"
@@ -1412,6 +1489,18 @@ dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pyte
docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"]
tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"]
[[package]]
name = "pyrfc3339"
version = "2.0.1"
description = "Generate and parse RFC 3339 timestamps"
optional = false
python-versions = "*"
groups = ["main"]
files = [
{file = "pyRFC3339-2.0.1-py3-none-any.whl", hash = "sha256:30b70a366acac3df7386b558c21af871522560ed7f3f73cf344b8c2cbb8b0c9d"},
{file = "pyrfc3339-2.0.1.tar.gz", hash = "sha256:e47843379ea35c1296c3b6c67a948a1a490ae0584edfcbdea0eaffb5dd29960b"},
]
[[package]] [[package]]
name = "pytest" name = "pytest"
version = "8.3.3" version = "8.3.3"
@@ -1604,6 +1693,18 @@ files = [
{file = "ruff-0.11.10.tar.gz", hash = "sha256:d522fb204b4959909ecac47da02830daec102eeb100fb50ea9554818d47a5fa6"}, {file = "ruff-0.11.10.tar.gz", hash = "sha256:d522fb204b4959909ecac47da02830daec102eeb100fb50ea9554818d47a5fa6"},
] ]
[[package]]
name = "semver"
version = "3.0.4"
description = "Python helper for Semantic Versioning (https://semver.org)"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "semver-3.0.4-py3-none-any.whl", hash = "sha256:9c824d87ba7f7ab4a1890799cec8596f15c1241cb473404ea1cb0c55e4b04746"},
{file = "semver-3.0.4.tar.gz", hash = "sha256:afc7d8c584a5ed0a11033af086e8af226a9c0b206f313e0301f8dd7b6b589602"},
]
[[package]] [[package]]
name = "six" name = "six"
version = "1.16.0" version = "1.16.0"
@@ -1628,6 +1729,24 @@ files = [
{file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"},
] ]
[[package]]
name = "starlette"
version = "0.46.2"
description = "The little ASGI library that shines."
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35"},
{file = "starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5"},
]
[package.dependencies]
anyio = ">=3.6.2,<5"
[package.extras]
full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"]
[[package]] [[package]]
name = "storage3" name = "storage3"
version = "0.11.0" version = "0.11.0"
@@ -1704,7 +1823,7 @@ description = "A lil' TOML parser"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
groups = ["main"] groups = ["main"]
markers = "python_version < \"3.11\"" markers = "python_version == \"3.10\""
files = [ files = [
{file = "tomli-2.1.0-py3-none-any.whl", hash = "sha256:a5c57c3d1c56f5ccdf89f6523458f60ef716e210fc47c4cfb188c5ba473e0391"}, {file = "tomli-2.1.0-py3-none-any.whl", hash = "sha256:a5c57c3d1c56f5ccdf89f6523458f60ef716e210fc47c4cfb188c5ba473e0391"},
{file = "tomli-2.1.0.tar.gz", hash = "sha256:3f646cae2aec94e17d04973e4249548320197cfabdf130015d023de4b74d8ab8"}, {file = "tomli-2.1.0.tar.gz", hash = "sha256:3f646cae2aec94e17d04973e4249548320197cfabdf130015d023de4b74d8ab8"},
@@ -1755,6 +1874,26 @@ h2 = ["h2 (>=4,<5)"]
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
zstd = ["zstandard (>=0.18.0)"] zstd = ["zstandard (>=0.18.0)"]
[[package]]
name = "uvicorn"
version = "0.34.3"
description = "The lightning-fast ASGI server."
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "uvicorn-0.34.3-py3-none-any.whl", hash = "sha256:16246631db62bdfbf069b0645177d6e8a77ba950cfedbfd093acef9444e4d885"},
{file = "uvicorn-0.34.3.tar.gz", hash = "sha256:35919a9a979d7a59334b6b10e05d77c1d0d574c50e0fc98b8b1a0f165708b55a"},
]
[package.dependencies]
click = ">=7.0"
h11 = ">=0.8"
typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""}
[package.extras]
standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.13)", "websockets (>=10.4)"]
[[package]] [[package]]
name = "websockets" name = "websockets"
version = "12.0" version = "12.0"
@@ -2037,4 +2176,4 @@ type = ["pytest-mypy"]
[metadata] [metadata]
lock-version = "2.1" lock-version = "2.1"
python-versions = ">=3.10,<4.0" python-versions = ">=3.10,<4.0"
content-hash = "78ebf65cdef769cfbe92fe204f01e32d219cca9ee5a6ca9e657aa0630be63802" content-hash = "d92143928a88ca3a56ac200c335910eafac938940022fed8bd0d17c95040b54f"

View File

@@ -17,6 +17,9 @@ pyjwt = "^2.10.1"
pytest-asyncio = "^0.26.0" pytest-asyncio = "^0.26.0"
pytest-mock = "^3.14.0" pytest-mock = "^3.14.0"
supabase = "^2.15.1" supabase = "^2.15.1"
launchdarkly-server-sdk = "^9.11.1"
fastapi = "^0.115.12"
uvicorn = "^0.34.3"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
redis = "^5.2.1" redis = "^5.2.1"

View File

@@ -9,7 +9,7 @@ from backend.blocks.exa._auth import (
) )
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField from backend.data.model import SchemaField
from backend.util.request import requests from backend.util.request import Requests
class ContentRetrievalSettings(BaseModel): class ContentRetrievalSettings(BaseModel):
@@ -79,7 +79,7 @@ class ExaContentsBlock(Block):
} }
try: try:
response = requests.post(url, headers=headers, json=payload) response = Requests().post(url, headers=headers, json=payload)
response.raise_for_status() response.raise_for_status()
data = response.json() data = response.json()
yield "results", data.get("results", []) yield "results", data.get("results", [])

View File

@@ -9,7 +9,7 @@ from backend.blocks.exa._auth import (
from backend.blocks.exa.helpers import ContentSettings from backend.blocks.exa.helpers import ContentSettings
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField from backend.data.model import SchemaField
from backend.util.request import requests from backend.util.request import Requests
class ExaSearchBlock(Block): class ExaSearchBlock(Block):
@@ -136,7 +136,7 @@ class ExaSearchBlock(Block):
payload[api_field] = value payload[api_field] = value
try: try:
response = requests.post(url, headers=headers, json=payload) response = Requests().post(url, headers=headers, json=payload)
response.raise_for_status() response.raise_for_status()
data = response.json() data = response.json()
# Extract just the results array from the response # Extract just the results array from the response

View File

@@ -8,7 +8,7 @@ from backend.blocks.exa._auth import (
) )
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField from backend.data.model import SchemaField
from backend.util.request import requests from backend.util.request import Requests
from .helpers import ContentSettings from .helpers import ContentSettings
@@ -120,7 +120,7 @@ class ExaFindSimilarBlock(Block):
payload[api_field] = value.strftime("%Y-%m-%dT%H:%M:%S.000Z") payload[api_field] = value.strftime("%Y-%m-%dT%H:%M:%S.000Z")
try: try:
response = requests.post(url, headers=headers, json=payload) response = Requests().post(url, headers=headers, json=payload)
response.raise_for_status() response.raise_for_status()
data = response.json() data = response.json()
yield "results", data.get("results", []) yield "results", data.get("results", [])

View File

@@ -114,9 +114,24 @@ class SendWebRequestBlock(Block):
body = input_data.body body = input_data.body
if isinstance(body, str): if isinstance(body, str):
try: try:
body = json.loads(body) # Validate JSON string length to prevent DoS attacks
except json.JSONDecodeError: if len(body) > 10_000_000: # 10MB limit
# plain text treat as formfield value instead raise ValueError("JSON body too large")
parsed_body = json.loads(body)
# Validate that parsed JSON is safe (basic object/array/primitive types)
if (
isinstance(parsed_body, (dict, list, str, int, float, bool))
or parsed_body is None
):
body = parsed_body
else:
# Unexpected type, treat as plain text
input_data.json_format = False
except (json.JSONDecodeError, ValueError):
# Invalid JSON or too large treat as formfield value instead
input_data.json_format = False input_data.json_format = False
# ─── Prepare files (if any) ────────────────────────────────── # ─── Prepare files (if any) ──────────────────────────────────

View File

@@ -5,7 +5,7 @@ from backend.blocks.hubspot._auth import (
) )
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField from backend.data.model import SchemaField
from backend.util.request import requests from backend.util.request import Requests
class HubSpotCompanyBlock(Block): class HubSpotCompanyBlock(Block):
@@ -45,7 +45,7 @@ class HubSpotCompanyBlock(Block):
} }
if input_data.operation == "create": if input_data.operation == "create":
response = requests.post( response = Requests().post(
base_url, headers=headers, json={"properties": input_data.company_data} base_url, headers=headers, json={"properties": input_data.company_data}
) )
result = response.json() result = response.json()
@@ -67,14 +67,14 @@ class HubSpotCompanyBlock(Block):
} }
] ]
} }
response = requests.post(search_url, headers=headers, json=search_data) response = Requests().post(search_url, headers=headers, json=search_data)
result = response.json() result = response.json()
yield "company", result.get("results", [{}])[0] yield "company", result.get("results", [{}])[0]
yield "status", "retrieved" yield "status", "retrieved"
elif input_data.operation == "update": elif input_data.operation == "update":
# First get company ID by domain # First get company ID by domain
search_response = requests.post( search_response = Requests().post(
f"{base_url}/search", f"{base_url}/search",
headers=headers, headers=headers,
json={ json={
@@ -94,7 +94,7 @@ class HubSpotCompanyBlock(Block):
company_id = search_response.json().get("results", [{}])[0].get("id") company_id = search_response.json().get("results", [{}])[0].get("id")
if company_id: if company_id:
response = requests.patch( response = Requests().patch(
f"{base_url}/{company_id}", f"{base_url}/{company_id}",
headers=headers, headers=headers,
json={"properties": input_data.company_data}, json={"properties": input_data.company_data},

View File

@@ -5,7 +5,7 @@ from backend.blocks.hubspot._auth import (
) )
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField from backend.data.model import SchemaField
from backend.util.request import requests from backend.util.request import Requests
class HubSpotContactBlock(Block): class HubSpotContactBlock(Block):
@@ -45,7 +45,7 @@ class HubSpotContactBlock(Block):
} }
if input_data.operation == "create": if input_data.operation == "create":
response = requests.post( response = Requests().post(
base_url, headers=headers, json={"properties": input_data.contact_data} base_url, headers=headers, json={"properties": input_data.contact_data}
) )
result = response.json() result = response.json()
@@ -68,13 +68,13 @@ class HubSpotContactBlock(Block):
} }
] ]
} }
response = requests.post(search_url, headers=headers, json=search_data) response = Requests().post(search_url, headers=headers, json=search_data)
result = response.json() result = response.json()
yield "contact", result.get("results", [{}])[0] yield "contact", result.get("results", [{}])[0]
yield "status", "retrieved" yield "status", "retrieved"
elif input_data.operation == "update": elif input_data.operation == "update":
search_response = requests.post( search_response = Requests().post(
f"{base_url}/search", f"{base_url}/search",
headers=headers, headers=headers,
json={ json={
@@ -94,7 +94,7 @@ class HubSpotContactBlock(Block):
contact_id = search_response.json().get("results", [{}])[0].get("id") contact_id = search_response.json().get("results", [{}])[0].get("id")
if contact_id: if contact_id:
response = requests.patch( response = Requests().patch(
f"{base_url}/{contact_id}", f"{base_url}/{contact_id}",
headers=headers, headers=headers,
json={"properties": input_data.contact_data}, json={"properties": input_data.contact_data},

View File

@@ -7,7 +7,7 @@ from backend.blocks.hubspot._auth import (
) )
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField from backend.data.model import SchemaField
from backend.util.request import requests from backend.util.request import Requests
class HubSpotEngagementBlock(Block): class HubSpotEngagementBlock(Block):
@@ -66,7 +66,7 @@ class HubSpotEngagementBlock(Block):
} }
} }
response = requests.post(email_url, headers=headers, json=email_data) response = Requests().post(email_url, headers=headers, json=email_data)
result = response.json() result = response.json()
yield "result", result yield "result", result
yield "status", "email_sent" yield "status", "email_sent"
@@ -80,7 +80,7 @@ class HubSpotEngagementBlock(Block):
params = {"limit": 100, "after": from_date.isoformat()} params = {"limit": 100, "after": from_date.isoformat()}
response = requests.get(engagement_url, headers=headers, params=params) response = Requests().get(engagement_url, headers=headers, params=params)
engagements = response.json() engagements = response.json()
# Process engagement metrics # Process engagement metrics

View File

@@ -12,7 +12,7 @@ from backend.data.model import (
SchemaField, SchemaField,
) )
from backend.integrations.providers import ProviderName from backend.integrations.providers import ProviderName
from backend.util.request import requests from backend.util.request import Requests
TEST_CREDENTIALS = APIKeyCredentials( TEST_CREDENTIALS = APIKeyCredentials(
id="01234567-89ab-cdef-0123-456789abcdef", id="01234567-89ab-cdef-0123-456789abcdef",
@@ -267,7 +267,7 @@ class IdeogramModelBlock(Block):
} }
try: try:
response = requests.post(url, json=data, headers=headers) response = Requests().post(url, json=data, headers=headers)
return response.json()["data"][0]["url"] return response.json()["data"][0]["url"]
except RequestException as e: except RequestException as e:
raise Exception(f"Failed to fetch image: {str(e)}") raise Exception(f"Failed to fetch image: {str(e)}")
@@ -280,14 +280,14 @@ class IdeogramModelBlock(Block):
try: try:
# Step 1: Download the image from the provided URL # Step 1: Download the image from the provided URL
image_response = requests.get(image_url) image_response = Requests().get(image_url)
# Step 2: Send the downloaded image to the upscale API # Step 2: Send the downloaded image to the upscale API
files = { files = {
"image_file": ("image.png", image_response.content, "image/png"), "image_file": ("image.png", image_response.content, "image/png"),
} }
response = requests.post( response = Requests().post(
url, url,
headers=headers, headers=headers,
data={"image_request": "{}"}, data={"image_request": "{}"},

View File

@@ -5,7 +5,7 @@ from backend.blocks.jina._auth import (
) )
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField from backend.data.model import SchemaField
from backend.util.request import requests from backend.util.request import Requests
class JinaChunkingBlock(Block): class JinaChunkingBlock(Block):
@@ -55,7 +55,7 @@ class JinaChunkingBlock(Block):
"max_chunk_length": str(input_data.max_chunk_length), "max_chunk_length": str(input_data.max_chunk_length),
} }
response = requests.post(url, headers=headers, json=data) response = Requests().post(url, headers=headers, json=data)
result = response.json() result = response.json()
all_chunks.extend(result.get("chunks", [])) all_chunks.extend(result.get("chunks", []))

View File

@@ -5,7 +5,7 @@ from backend.blocks.jina._auth import (
) )
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField from backend.data.model import SchemaField
from backend.util.request import requests from backend.util.request import Requests
class JinaEmbeddingBlock(Block): class JinaEmbeddingBlock(Block):
@@ -38,6 +38,6 @@ class JinaEmbeddingBlock(Block):
"Authorization": f"Bearer {credentials.api_key.get_secret_value()}", "Authorization": f"Bearer {credentials.api_key.get_secret_value()}",
} }
data = {"input": input_data.texts, "model": input_data.model} data = {"input": input_data.texts, "model": input_data.model}
response = requests.post(url, headers=headers, json=data) response = Requests().post(url, headers=headers, json=data)
embeddings = [e["embedding"] for e in response.json()["data"]] embeddings = [e["embedding"] for e in response.json()["data"]]
yield "embeddings", embeddings yield "embeddings", embeddings

View File

@@ -1,7 +1,5 @@
from urllib.parse import quote from urllib.parse import quote
import requests
from backend.blocks.jina._auth import ( from backend.blocks.jina._auth import (
JinaCredentials, JinaCredentials,
JinaCredentialsField, JinaCredentialsField,
@@ -9,6 +7,7 @@ from backend.blocks.jina._auth import (
) )
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField from backend.data.model import SchemaField
from backend.util.request import requests
class FactCheckerBlock(Block): class FactCheckerBlock(Block):

View File

@@ -13,7 +13,7 @@ from backend.data.model import (
SecretField, SecretField,
) )
from backend.integrations.providers import ProviderName from backend.integrations.providers import ProviderName
from backend.util.request import requests from backend.util.request import Requests
TEST_CREDENTIALS = APIKeyCredentials( TEST_CREDENTIALS = APIKeyCredentials(
id="01234567-89ab-cdef-0123-456789abcdef", id="01234567-89ab-cdef-0123-456789abcdef",
@@ -160,7 +160,7 @@ class PublishToMediumBlock(Block):
"notifyFollowers": notify_followers, "notifyFollowers": notify_followers,
} }
response = requests.post( response = Requests().post(
f"https://api.medium.com/v1/users/{author_id}/posts", f"https://api.medium.com/v1/users/{author_id}/posts",
headers=headers, headers=headers,
json=data, json=data,

View File

@@ -5,7 +5,7 @@ from backend.blocks.nvidia._auth import (
) )
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField from backend.data.model import SchemaField
from backend.util.request import requests from backend.util.request import Requests
from backend.util.type import MediaFileType from backend.util.type import MediaFileType
@@ -59,7 +59,7 @@ class NvidiaDeepfakeDetectBlock(Block):
} }
try: try:
response = requests.post(url, headers=headers, json=payload) response = Requests().post(url, headers=headers, json=payload)
response.raise_for_status() response.raise_for_status()
data = response.json() data = response.json()

View File

@@ -121,11 +121,10 @@ class ScreenshotWebPageBlock(Block):
""" """
Takes a screenshot using the ScreenshotOne API Takes a screenshot using the ScreenshotOne API
""" """
api = Requests(trusted_origins=["https://api.screenshotone.com"]) api = Requests()
# Build API URL with parameters # Build API parameters
params = { params = {
"access_key": credentials.api_key.get_secret_value(),
"url": url, "url": url,
"viewport_width": viewport_width, "viewport_width": viewport_width,
"viewport_height": viewport_height, "viewport_height": viewport_height,
@@ -137,7 +136,14 @@ class ScreenshotWebPageBlock(Block):
"cache": str(cache).lower(), "cache": str(cache).lower(),
} }
response = api.get("https://api.screenshotone.com/take", params=params) # Use header-based authentication instead of query parameter
headers = {
"X-Access-Key": credentials.api_key.get_secret_value(),
}
response = api.get(
"https://api.screenshotone.com/take", params=params, headers=headers
)
return { return {
"image": store_media_file( "image": store_media_file(

View File

@@ -1,7 +1,7 @@
from typing import Any, Dict from typing import Any, Dict
from backend.data.block import Block from backend.data.block import Block
from backend.util.request import requests from backend.util.request import Requests
from ._api import Color, CustomerDetails, OrderItem, Profile from ._api import Color, CustomerDetails, OrderItem, Profile
@@ -16,7 +16,7 @@ class Slant3DBlockBase(Block):
def _make_request(self, method: str, endpoint: str, api_key: str, **kwargs) -> Dict: def _make_request(self, method: str, endpoint: str, api_key: str, **kwargs) -> Dict:
url = f"{self.BASE_URL}/{endpoint}" url = f"{self.BASE_URL}/{endpoint}"
response = requests.request( response = Requests().request(
method=method, url=url, headers=self._get_headers(api_key), **kwargs method=method, url=url, headers=self._get_headers(api_key), **kwargs
) )

View File

@@ -1,11 +1,10 @@
import uuid import uuid
from typing import List from typing import List
import requests as baserequests
from backend.data.block import BlockOutput, BlockSchema from backend.data.block import BlockOutput, BlockSchema
from backend.data.model import APIKeyCredentials, SchemaField from backend.data.model import APIKeyCredentials, SchemaField
from backend.util import settings from backend.util import settings
from backend.util.request import req
from backend.util.settings import BehaveAs from backend.util.settings import BehaveAs
from ._api import ( from ._api import (
@@ -181,7 +180,7 @@ class Slant3DEstimateOrderBlock(Slant3DBlockBase):
yield "total_price", result["totalPrice"] yield "total_price", result["totalPrice"]
yield "shipping_cost", result["shippingCost"] yield "shipping_cost", result["shippingCost"]
yield "printing_cost", result["printingCost"] yield "printing_cost", result["printingCost"]
except baserequests.HTTPError as e: except req.HTTPError as e:
yield "error", str(f"Error estimating order: {e} {e.response.text}") yield "error", str(f"Error estimating order: {e} {e.response.text}")
raise raise

View File

@@ -121,16 +121,17 @@ def reddit(server_address: str):
""" """
Create an event graph Create an event graph
""" """
import requests
from backend.usecases.reddit_marketing import create_test_graph from backend.usecases.reddit_marketing import create_test_graph
from backend.util.request import Requests
test_graph = create_test_graph() test_graph = create_test_graph()
url = f"{server_address}/graphs" url = f"{server_address}/graphs"
headers = {"Content-Type": "application/json"} headers = {"Content-Type": "application/json"}
data = test_graph.model_dump_json() data = test_graph.model_dump_json()
response = requests.post(url, headers=headers, data=data) response = Requests(trusted_origins=[server_address]).post(
url, headers=headers, data=data
)
graph_id = response.json()["id"] graph_id = response.json()["id"]
print(f"Graph created with ID: {graph_id}") print(f"Graph created with ID: {graph_id}")
@@ -142,16 +143,18 @@ def populate_db(server_address: str):
""" """
Create an event graph Create an event graph
""" """
import requests
from backend.usecases.sample import create_test_graph from backend.usecases.sample import create_test_graph
from backend.util.request import Requests
test_graph = create_test_graph() test_graph = create_test_graph()
url = f"{server_address}/graphs" url = f"{server_address}/graphs"
headers = {"Content-Type": "application/json"} headers = {"Content-Type": "application/json"}
data = test_graph.model_dump_json() data = test_graph.model_dump_json()
response = requests.post(url, headers=headers, data=data) response = Requests(trusted_origins=[server_address]).post(
url, headers=headers, data=data
)
graph_id = response.json()["id"] graph_id = response.json()["id"]
@@ -159,7 +162,9 @@ def populate_db(server_address: str):
execute_url = f"{server_address}/graphs/{response.json()['id']}/execute" execute_url = f"{server_address}/graphs/{response.json()['id']}/execute"
text = "Hello, World!" text = "Hello, World!"
input_data = {"input": text} input_data = {"input": text}
response = requests.post(execute_url, headers=headers, json=input_data) response = Requests(trusted_origins=[server_address]).post(
execute_url, headers=headers, json=input_data
)
schedule_url = f"{server_address}/graphs/{graph_id}/schedules" schedule_url = f"{server_address}/graphs/{graph_id}/schedules"
data = { data = {
@@ -167,7 +172,9 @@ def populate_db(server_address: str):
"cron": "*/5 * * * *", "cron": "*/5 * * * *",
"input_data": {"input": "Hello, World!"}, "input_data": {"input": "Hello, World!"},
} }
response = requests.post(schedule_url, headers=headers, json=data) response = Requests(trusted_origins=[server_address]).post(
schedule_url, headers=headers, json=data
)
print("Database populated with: \n- graph\n- execution\n- schedule") print("Database populated with: \n- graph\n- execution\n- schedule")
@@ -178,21 +185,25 @@ def graph(server_address: str):
""" """
Create an event graph Create an event graph
""" """
import requests
from backend.usecases.sample import create_test_graph from backend.usecases.sample import create_test_graph
from backend.util.request import Requests
url = f"{server_address}/graphs" url = f"{server_address}/graphs"
headers = {"Content-Type": "application/json"} headers = {"Content-Type": "application/json"}
data = create_test_graph().model_dump_json() data = create_test_graph().model_dump_json()
response = requests.post(url, headers=headers, data=data) response = Requests(trusted_origins=[server_address]).post(
url, headers=headers, data=data
)
if response.status_code == 200: if response.status_code == 200:
print(response.json()["id"]) print(response.json()["id"])
execute_url = f"{server_address}/graphs/{response.json()['id']}/execute" execute_url = f"{server_address}/graphs/{response.json()['id']}/execute"
text = "Hello, World!" text = "Hello, World!"
input_data = {"input": text} input_data = {"input": text}
response = requests.post(execute_url, headers=headers, json=input_data) response = Requests(trusted_origins=[server_address]).post(
execute_url, headers=headers, json=input_data
)
else: else:
print("Failed to send graph") print("Failed to send graph")
@@ -206,12 +217,15 @@ def execute(graph_id: str, content: dict):
""" """
Create an event graph Create an event graph
""" """
import requests
from backend.util.request import Requests
headers = {"Content-Type": "application/json"} headers = {"Content-Type": "application/json"}
execute_url = f"http://0.0.0.0:8000/graphs/{graph_id}/execute" execute_url = f"http://0.0.0.0:8000/graphs/{graph_id}/execute"
requests.post(execute_url, headers=headers, json=content) Requests(trusted_origins=["http://0.0.0.0:8000"]).post(
execute_url, headers=headers, json=content
)
@test.command() @test.command()

View File

@@ -398,7 +398,7 @@ class IntegrationCredentialsStore:
( (
state state
for state in oauth_states for state in oauth_states
if state.token == token if secrets.compare_digest(state.token, token)
and state.provider == provider and state.provider == provider
and state.expires_at > now.timestamp() and state.expires_at > now.timestamp()
), ),

View File

@@ -4,7 +4,7 @@ from urllib.parse import urlencode
from backend.data.model import OAuth2Credentials from backend.data.model import OAuth2Credentials
from backend.integrations.providers import ProviderName from backend.integrations.providers import ProviderName
from backend.util.request import requests from backend.util.request import Requests
from .base import BaseOAuthHandler from .base import BaseOAuthHandler
@@ -59,7 +59,7 @@ class GitHubOAuthHandler(BaseOAuthHandler):
"X-GitHub-Api-Version": "2022-11-28", "X-GitHub-Api-Version": "2022-11-28",
} }
requests.delete( Requests().delete(
url=self.revoke_url.format(client_id=self.client_id), url=self.revoke_url.format(client_id=self.client_id),
auth=(self.client_id, self.client_secret), auth=(self.client_id, self.client_secret),
headers=headers, headers=headers,
@@ -89,7 +89,7 @@ class GitHubOAuthHandler(BaseOAuthHandler):
**params, **params,
} }
headers = {"Accept": "application/json"} headers = {"Accept": "application/json"}
response = requests.post(self.token_url, data=request_body, headers=headers) response = Requests().post(self.token_url, data=request_body, headers=headers)
token_data: dict = response.json() token_data: dict = response.json()
username = self._request_username(token_data["access_token"]) username = self._request_username(token_data["access_token"])
@@ -132,7 +132,7 @@ class GitHubOAuthHandler(BaseOAuthHandler):
"X-GitHub-Api-Version": "2022-11-28", "X-GitHub-Api-Version": "2022-11-28",
} }
response = requests.get(url, headers=headers) response = Requests().get(url, headers=headers)
if not response.ok: if not response.ok:
return None return None

View File

@@ -76,7 +76,7 @@ class GoogleOAuthHandler(BaseOAuthHandler):
logger.debug(f"Scopes granted by Google: {granted_scopes}") logger.debug(f"Scopes granted by Google: {granted_scopes}")
google_creds = flow.credentials google_creds = flow.credentials
logger.debug(f"Received credentials: {google_creds}") logger.debug("Received credentials")
logger.debug("Requesting user email") logger.debug("Requesting user email")
username = self._request_email(google_creds) username = self._request_email(google_creds)

View File

@@ -7,7 +7,7 @@ from pydantic import SecretStr
from backend.blocks.linear._api import LinearAPIException from backend.blocks.linear._api import LinearAPIException
from backend.data.model import APIKeyCredentials, OAuth2Credentials from backend.data.model import APIKeyCredentials, OAuth2Credentials
from backend.integrations.providers import ProviderName from backend.integrations.providers import ProviderName
from backend.util.request import requests from backend.util.request import Requests
from .base import BaseOAuthHandler from .base import BaseOAuthHandler
@@ -53,7 +53,7 @@ class LinearOAuthHandler(BaseOAuthHandler):
"Authorization": f"Bearer {credentials.access_token.get_secret_value()}" "Authorization": f"Bearer {credentials.access_token.get_secret_value()}"
} }
response = requests.post(self.revoke_url, headers=headers) response = Requests().post(self.revoke_url, headers=headers)
if not response.ok: if not response.ok:
try: try:
error_data = response.json() error_data = response.json()
@@ -95,7 +95,7 @@ class LinearOAuthHandler(BaseOAuthHandler):
headers = { headers = {
"Content-Type": "application/x-www-form-urlencoded" "Content-Type": "application/x-www-form-urlencoded"
} # Correct header for token request } # Correct header for token request
response = requests.post(self.token_url, data=request_body, headers=headers) response = Requests().post(self.token_url, data=request_body, headers=headers)
if not response.ok: if not response.ok:
try: try:

View File

@@ -4,7 +4,7 @@ from urllib.parse import urlencode
from backend.data.model import OAuth2Credentials from backend.data.model import OAuth2Credentials
from backend.integrations.providers import ProviderName from backend.integrations.providers import ProviderName
from backend.util.request import requests from backend.util.request import Requests
from .base import BaseOAuthHandler from .base import BaseOAuthHandler
@@ -52,7 +52,7 @@ class NotionOAuthHandler(BaseOAuthHandler):
"Authorization": f"Basic {auth_str}", "Authorization": f"Basic {auth_str}",
"Accept": "application/json", "Accept": "application/json",
} }
response = requests.post(self.token_url, json=request_body, headers=headers) response = Requests().post(self.token_url, json=request_body, headers=headers)
token_data = response.json() token_data = response.json()
# Email is only available for non-bot users # Email is only available for non-bot users
email = ( email = (

View File

@@ -1,10 +1,9 @@
import urllib.parse import urllib.parse
from typing import ClassVar, Optional from typing import ClassVar, Optional
import requests
from backend.data.model import OAuth2Credentials, ProviderName from backend.data.model import OAuth2Credentials, ProviderName
from backend.integrations.oauth.base import BaseOAuthHandler from backend.integrations.oauth.base import BaseOAuthHandler
from backend.util.request import Requests
class TodoistOAuthHandler(BaseOAuthHandler): class TodoistOAuthHandler(BaseOAuthHandler):
@@ -48,12 +47,12 @@ class TodoistOAuthHandler(BaseOAuthHandler):
"redirect_uri": self.redirect_uri, "redirect_uri": self.redirect_uri,
} }
response = requests.post(self.TOKEN_URL, data=data) response = Requests().post(self.TOKEN_URL, data=data)
response.raise_for_status() response.raise_for_status()
tokens = response.json() tokens = response.json()
response = requests.post( response = Requests().post(
"https://api.todoist.com/sync/v9/sync", "https://api.todoist.com/sync/v9/sync",
headers={"Authorization": f"Bearer {tokens['access_token']}"}, headers={"Authorization": f"Bearer {tokens['access_token']}"},
data={"sync_token": "*", "resource_types": '["user"]'}, data={"sync_token": "*", "resource_types": '["user"]'},

View File

@@ -2,10 +2,9 @@ import time
import urllib.parse import urllib.parse
from typing import ClassVar, Optional from typing import ClassVar, Optional
import requests
from backend.data.model import OAuth2Credentials, ProviderName from backend.data.model import OAuth2Credentials, ProviderName
from backend.integrations.oauth.base import BaseOAuthHandler from backend.integrations.oauth.base import BaseOAuthHandler
from backend.util.request import Requests, req
class TwitterOAuthHandler(BaseOAuthHandler): class TwitterOAuthHandler(BaseOAuthHandler):
@@ -78,7 +77,9 @@ class TwitterOAuthHandler(BaseOAuthHandler):
auth = (self.client_id, self.client_secret) auth = (self.client_id, self.client_secret)
response = requests.post(self.TOKEN_URL, headers=headers, data=data, auth=auth) response = Requests().post(
self.TOKEN_URL, headers=headers, data=data, auth=auth
)
response.raise_for_status() response.raise_for_status()
tokens = response.json() tokens = response.json()
@@ -102,7 +103,7 @@ class TwitterOAuthHandler(BaseOAuthHandler):
params = {"user.fields": "username"} params = {"user.fields": "username"}
response = requests.get( response = Requests().get(
f"{self.USERNAME_URL}?{urllib.parse.urlencode(params)}", headers=headers f"{self.USERNAME_URL}?{urllib.parse.urlencode(params)}", headers=headers
) )
response.raise_for_status() response.raise_for_status()
@@ -122,13 +123,12 @@ class TwitterOAuthHandler(BaseOAuthHandler):
auth = (self.client_id, self.client_secret) auth = (self.client_id, self.client_secret)
response = requests.post(self.TOKEN_URL, headers=header, data=data, auth=auth) response = Requests().post(self.TOKEN_URL, headers=header, data=data, auth=auth)
try: try:
response.raise_for_status() response.raise_for_status()
except requests.exceptions.HTTPError as e: except req.exceptions.HTTPError:
print("HTTP Error:", e) print(f"HTTP Error: {response.status_code}")
print("Response Content:", response.text)
raise raise
tokens = response.json() tokens = response.json()
@@ -159,13 +159,14 @@ class TwitterOAuthHandler(BaseOAuthHandler):
auth = (self.client_id, self.client_secret) auth = (self.client_id, self.client_secret)
response = requests.post(self.REVOKE_URL, headers=header, data=data, auth=auth) response = Requests().post(
self.REVOKE_URL, headers=header, data=data, auth=auth
)
try: try:
response.raise_for_status() response.raise_for_status()
except requests.exceptions.HTTPError as e: except req.exceptions.HTTPError:
print("HTTP Error:", e) print(f"HTTP Error: {response.status_code}")
print("Response Content:", response.text)
raise raise
return response.status_code == 200 return response.status_code == 200

View File

@@ -2,13 +2,13 @@ import hashlib
import hmac import hmac
import logging import logging
import requests
from fastapi import HTTPException, Request from fastapi import HTTPException, Request
from strenum import StrEnum from strenum import StrEnum
from backend.data import integrations from backend.data import integrations
from backend.data.model import Credentials from backend.data.model import Credentials
from backend.integrations.providers import ProviderName from backend.integrations.providers import ProviderName
from backend.util.request import Requests, req
from ._base import BaseWebhooksManager from ._base import BaseWebhooksManager
@@ -73,7 +73,7 @@ class GithubWebhooksManager(BaseWebhooksManager):
repo, github_hook_id = webhook.resource, webhook.provider_webhook_id repo, github_hook_id = webhook.resource, webhook.provider_webhook_id
ping_url = f"{self.GITHUB_API_URL}/repos/{repo}/hooks/{github_hook_id}/pings" ping_url = f"{self.GITHUB_API_URL}/repos/{repo}/hooks/{github_hook_id}/pings"
response = requests.post(ping_url, headers=headers) response = Requests().post(ping_url, headers=headers)
if response.status_code != 204: if response.status_code != 204:
error_msg = extract_github_error_msg(response) error_msg = extract_github_error_msg(response)
@@ -110,7 +110,7 @@ class GithubWebhooksManager(BaseWebhooksManager):
}, },
} }
response = requests.post( response = Requests().post(
f"{self.GITHUB_API_URL}/repos/{resource}/hooks", f"{self.GITHUB_API_URL}/repos/{resource}/hooks",
headers=headers, headers=headers,
json=webhook_data, json=webhook_data,
@@ -153,7 +153,7 @@ class GithubWebhooksManager(BaseWebhooksManager):
f"Unsupported webhook type '{webhook.webhook_type}'" f"Unsupported webhook type '{webhook.webhook_type}'"
) )
response = requests.delete(delete_url, headers=headers) response = Requests().delete(delete_url, headers=headers)
if response.status_code not in [204, 404]: if response.status_code not in [204, 404]:
# 204 means successful deletion, 404 means the webhook was already deleted # 204 means successful deletion, 404 means the webhook was already deleted
@@ -166,7 +166,7 @@ class GithubWebhooksManager(BaseWebhooksManager):
# --8<-- [end:GithubWebhooksManager] # --8<-- [end:GithubWebhooksManager]
def extract_github_error_msg(response: requests.Response) -> str: def extract_github_error_msg(response: req.Response) -> str:
error_msgs = [] error_msgs = []
resp = response.json() resp = response.json()
if resp.get("message"): if resp.get("message"):

View File

@@ -1,12 +1,12 @@
import logging import logging
import requests
from fastapi import Request from fastapi import Request
from backend.data import integrations from backend.data import integrations
from backend.data.model import APIKeyCredentials, Credentials from backend.data.model import APIKeyCredentials, Credentials
from backend.integrations.providers import ProviderName from backend.integrations.providers import ProviderName
from backend.integrations.webhooks._base import BaseWebhooksManager from backend.integrations.webhooks._base import BaseWebhooksManager
from backend.util.request import Requests
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -39,7 +39,7 @@ class Slant3DWebhooksManager(BaseWebhooksManager):
# Slant3D's API doesn't use events list, just register for all order updates # Slant3D's API doesn't use events list, just register for all order updates
payload = {"endPoint": ingress_url} payload = {"endPoint": ingress_url}
response = requests.post( response = Requests().post(
f"{self.BASE_URL}/customer/webhookSubscribe", headers=headers, json=payload f"{self.BASE_URL}/customer/webhookSubscribe", headers=headers, json=payload
) )

View File

@@ -36,11 +36,11 @@ logger = logging.getLogger(__name__)
@router.post("/unsubscribe") @router.post("/unsubscribe")
async def unsubscribe_via_one_click(token: Annotated[str, Query()]): async def unsubscribe_via_one_click(token: Annotated[str, Query()]):
logger.info(f"Received unsubscribe request from One Click Unsubscribe: {token}") logger.info("Received unsubscribe request from One Click Unsubscribe")
try: try:
await unsubscribe_user_by_token(token) await unsubscribe_user_by_token(token)
except Exception as e: except Exception as e:
logger.exception("Unsubscribe token %s failed: %s", token, e) logger.exception("Unsubscribe failed: %s", e)
raise HTTPException( raise HTTPException(
status_code=500, status_code=500,
detail={"message": str(e), "hint": "Verify Postmark token settings."}, detail={"message": str(e), "hint": "Verify Postmark token settings."},

View File

@@ -303,13 +303,16 @@ develop = true
[package.dependencies] [package.dependencies]
colorama = "^0.4.6" colorama = "^0.4.6"
expiringdict = "^1.2.2" expiringdict = "^1.2.2"
fastapi = "^0.115.12"
google-cloud-logging = "^3.12.1" google-cloud-logging = "^3.12.1"
launchdarkly-server-sdk = "^9.11.1"
pydantic = "^2.11.4" pydantic = "^2.11.4"
pydantic-settings = "^2.9.1" pydantic-settings = "^2.9.1"
pyjwt = "^2.10.1" pyjwt = "^2.10.1"
pytest-asyncio = "^0.26.0" pytest-asyncio = "^0.26.0"
pytest-mock = "^3.14.0" pytest-mock = "^3.14.0"
supabase = "^2.15.1" supabase = "^2.15.1"
uvicorn = "^0.34.3"
[package.source] [package.source]
type = "directory" type = "directory"

View File

@@ -11,13 +11,25 @@ async function shouldShowOnboarding() {
); );
} }
// Validate redirect URL to prevent open redirect attacks
function validateRedirectUrl(url: string): string {
// Only allow relative URLs that start with /
if (url.startsWith("/") && !url.startsWith("//")) {
return url;
}
// Default to home page for any invalid URLs
return "/";
}
// Handle the callback to complete the user session login // Handle the callback to complete the user session login
export async function GET(request: Request) { export async function GET(request: Request) {
const { searchParams, origin } = new URL(request.url); const { searchParams, origin } = new URL(request.url);
const code = searchParams.get("code"); const code = searchParams.get("code");
// if "next" is in param, use it as the redirect URL // if "next" is in param, use it as the redirect URL
let next = searchParams.get("next") ?? "/"; const nextParam = searchParams.get("next") ?? "/";
// Validate redirect URL to prevent open redirect attacks
let next = validateRedirectUrl(nextParam);
if (code) { if (code) {
const supabase = await getServerSupabase(); const supabase = await getServerSupabase();

View File

@@ -27,6 +27,7 @@ import {
cn, cn,
getValue, getValue,
hasNonNullNonObjectValue, hasNonNullNonObjectValue,
isObject,
parseKeys, parseKeys,
setNestedProperty, setNestedProperty,
} from "@/lib/utils"; } from "@/lib/utils";
@@ -435,8 +436,15 @@ export const CustomNode = React.memo(
if (activeKey) { if (activeKey) {
try { try {
const parsedValue = JSON.parse(value); const parsedValue = JSON.parse(value);
handleInputChange(activeKey, parsedValue); // Validate that the parsed value is safe before using it
if (isObject(parsedValue) || Array.isArray(parsedValue)) {
handleInputChange(activeKey, parsedValue);
} else {
// For primitive values, use the original string
handleInputChange(activeKey, value);
}
} catch (error) { } catch (error) {
// If JSON parsing fails, treat as plain text
handleInputChange(activeKey, value); handleInputChange(activeKey, value);
} }
} }

View File

@@ -396,3 +396,8 @@ export function getValue(key: string, value: any) {
export function isEmptyOrWhitespace(str: string | undefined | null): boolean { export function isEmptyOrWhitespace(str: string | undefined | null): boolean {
return !str || str.trim().length === 0; return !str || str.trim().length === 0;
} }
/** Chech if a value is an object or not */
export function isObject(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}