Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 62 additions & 40 deletions src/Engine/lib/api/routers/SettingsRouter.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,32 @@
from logging import Logger, getLogger
from os.path import exists
from shutil import rmtree
from typing import Literal
from os.path import exists

from fastapi import APIRouter
from fastapi.responses import JSONResponse

from ...settings.SavedSettings import AISettings
from ..validation import AIProviderKeyData, SetDefaultProviderData, GeneralSettingsData
from ...settings import settings
from ...settings.SavedSettings import AISettings
from ..validation import AIProviderKeyData, GeneralSettingsData, SetDefaultProviderData

logger: Logger = getLogger(f"fastapi.{__name__}")

router: APIRouter = APIRouter(prefix="/settings", tags=["Settings"])


@router.post("/general", operation_id="set_general_settings", summary="Update general application settings.")
@router.post(
"/general",
operation_id="set_general_settings",
summary="Update general application settings.",
)
def set_general_settings(data: GeneralSettingsData) -> JSONResponse:
"""
Update general application settings.

Args:
data.login_method (str): Login method to use - 'cookies' or 'profiles'

Returns:
success (bool): True if settings were saved successfully
message (str): Confirmation message
Expand Down Expand Up @@ -53,26 +57,30 @@
)


@router.delete("/general/clear-login-data", operation_id="clear_login_data", summary="Clear all login data including cookies and profiles.")
@router.delete(
"/general/clear-login-data",
operation_id="clear_login_data",
summary="Clear all login data including cookies and profiles.",
)
def clear_login_data() -> JSONResponse:
"""
Clear all login data from the Environment directory.

This removes:
- cookies.json (saved browser cookies)
- All temp profiles
- Base profile
- data.json (channel data)

Returns:
success (bool): True if data was cleared successfully
message (str): Confirmation or error message
"""
logger.info("Clearing all login data from Environment directory")

try:
environment_dir = settings.environment_dir

if not exists(environment_dir):
return JSONResponse(
status_code=200,
Expand All @@ -81,38 +89,44 @@
"message": "No login data found to clear",
},
)

# Remove the entire Environment directory and its contents
rmtree(environment_dir)
logger.info(f"Cleared Environment directory: {environment_dir}")

return JSONResponse(
status_code=200,
content={
"success": True,
"message": "All login data cleared successfully",
},
)

except Exception as e:
logger.error(f"Error clearing login data: {e}")

return JSONResponse(
status_code=500,
content={"success": False, "message": f"Error clearing login data: {str(e)}"},
content={
"success": False,
"message": f"Error clearing login data: {str(e)}",
},
Comment on lines +110 to +113
)
Comment on lines 105 to 114

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Exception details leaked to API response.

str(e) is embedded directly into the client-facing message field. Static analysis (CodeQL) flags this as information exposure through an exception — internal paths or implementation details from rmtree/filesystem errors could leak to the client. Log the full exception server-side (already done via logger.error) and return a generic message to the caller.

🔒️ Proposed fix
     except Exception as e:
         logger.error(f"Error clearing login data: {e}")

         return JSONResponse(
             status_code=500,
             content={
                 "success": False,
-                "message": f"Error clearing login data: {str(e)}",
+                "message": "Error clearing login data. Please check the server logs for details.",
             },
         )

Note: this same pattern (raw str(e) returned to the client) recurs in the other exception handlers in this file (e.g. get_settings, get_ai_keys, save_ai_key, set_default_provider); consider fixing consistently across the file.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
except Exception as e:
logger.error(f"Error clearing login data: {e}")
return JSONResponse(
status_code=500,
content={"success": False, "message": f"Error clearing login data: {str(e)}"},
content={
"success": False,
"message": f"Error clearing login data: {str(e)}",
},
)
except Exception as e:
logger.error(f"Error clearing login data: {e}")
return JSONResponse(
status_code=500,
content={
"success": False,
"message": "Error clearing login data. Please check the server logs for details.",
},
)
🧰 Tools
🪛 GitHub Check: CodeQL

[warning] 110-113: Information exposure through an exception
Stack trace information flows to this location and may be exposed to an external user.

🪛 Ruff (0.15.20)

[warning] 105-105: Do not catch blind exception: Exception

(BLE001)


[warning] 112-112: Use explicit conversion flag

Replace with conversion flag

(RUF010)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Engine/lib/api/routers/SettingsRouter.py` around lines 105 - 114, The
exception handlers in SettingsRouter are returning raw exception text to clients
via the response message, which can leak internal details. Keep the existing
logger.error calls for server-side diagnostics, but update the client-facing
JSONResponse in this handler to use a generic failure message instead of str(e).
Apply the same pattern consistently across the other exception blocks in
SettingsRouter, including get_settings, get_ai_keys, save_ai_key, and
set_default_provider.

Source: Linters/SAST tools




@router.get("/", operation_id="get_settings", summary="Get all application settings including engine config, AI provider, and general settings.")
@router.get(
"/",
operation_id="get_settings",
summary="Get all application settings including engine config, AI provider, and general settings.",
)
def get_settings() -> JSONResponse:
"""
Get all application settings.
Returns engine configuration (version, log file path),

Returns engine configuration (version, log file path),
default AI provider settings under the 'ai' sub-key,
and general settings under the 'general' sub-key.

Returns:
success (bool): True if the request was successful
version (str): StreamStorm engine version
Expand Down Expand Up @@ -154,14 +168,18 @@
)


@router.get("/ai/keys", operation_id="get_ai_provider_keys", summary="Get all AI provider configurations and keys.")
@router.get(
"/ai/keys",
operation_id="get_ai_provider_keys",
summary="Get all AI provider configurations and keys.",
)
def get_ai_keys() -> JSONResponse:
"""
Get all AI provider configurations and keys.

Returns the complete AI settings including API keys (masked),
models, base URLs, and default provider configuration.

Returns:
success (bool): True if the request was successful
providers (dict): Configuration for each AI provider (openai, anthropic, google)
Expand All @@ -175,8 +193,7 @@
logger.info("AI provider keys fetched successfully")

return JSONResponse(
status_code=200,
content={"success": True, **settings.ai.model_dump()}
status_code=200, content={"success": True, **settings.ai.model_dump()}
)

except Exception as e:
Expand All @@ -188,23 +205,27 @@
)


@router.post("/ai/keys/{provider_id}", operation_id="save_ai_provider_key", summary="Save API key and settings for an AI provider.")
@router.post(
"/ai/keys/{provider_id}",
operation_id="save_ai_provider_key",
summary="Save API key and settings for an AI provider.",
)
def save_ai_key(
provider_id: Literal["openai", "anthropic", "google"], data: AIProviderKeyData
) -> JSONResponse:
"""
Save API key and settings for a specific AI provider.

Updates the configuration for the specified AI provider including
API key, model, and optionally base URL. If the provider is the
current default, also updates the default model.

Args:
provider_id (str): Provider to configure - 'openai', 'anthropic', or 'google'
data.api_key (str): API key for the provider
data.model (str): Model name to use
data.base_url (str, optional): Custom base URL for the API

Returns:
success (bool): True if settings were saved successfully
message (str): Confirmation message
Expand All @@ -228,6 +249,7 @@
# If this provider is the current default, update defaultModel as well
if ai_settings.defaultProvider == provider_id:
ai_settings.defaultModel = data.model
ai_settings.defaultBaseUrl = data.base_url

logger.info(f"Updated defaultModel to: {data.model}")

Expand All @@ -246,35 +268,38 @@

except Exception as e:
logger.error(f"Error saving AI key for {provider_id}: {e}")

return JSONResponse(
status_code=500,
content={"success": False, "message": f"Error saving settings: {str(e)}"},
)



@router.post("/ai/default", operation_id="set_default_ai_provider", summary="Set the default AI provider for message generation.")
@router.post(
"/ai/default",
operation_id="set_default_ai_provider",
summary="Set the default AI provider for message generation.",
)
def set_default_provider(data: SetDefaultProviderData) -> JSONResponse:
"""
Set the default AI provider for message generation.

Updates the default AI provider, model, and base URL used for
generating messages and channel names via AI.

Args:
data.provider (str): Provider to set as default - 'openai', 'anthropic', or 'google'
data.model (str): Model name to use with the provider
data.base_url (str): Base URL for the provider API

Returns:
success (bool): True if the default was set successfully
message (str): Confirmation message
defaultProvider (str): Updated default provider
defaultModel (str): Updated default model
defaultBaseUrl (str): Updated base URL
"""

logger.info(
f"Setting default AI provider to: {data.provider} with model: {data.model}"
)
Expand Down Expand Up @@ -314,6 +339,3 @@
"message": f"Error setting default provider: {str(e)}",
},
)



24 changes: 22 additions & 2 deletions src/UI/src/Components/Modals/Settings/Sections/ApiKeysSettings.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ const ApiKeySection = ({ provider, expanded, onExpand, apiKeysData, onUpdateApiK
const [baseUrlHelperText, setBaseUrlHelperText] = useState('');
const [modelError, setModelError] = useState(false);
const [modelHelperText, setModelHelperText] = useState('');
const [customModelError, setCustomModelError] = useState(false);
const [customModelHelperText, setCustomModelHelperText] = useState('');

// Check if using custom URL (for OpenAI)
const isCustomUrl = provider.hasBaseUrl && baseUrl.trim() !== '' && baseUrl.trim() !== DEFAULT_OPENAI_URL;
Expand Down Expand Up @@ -155,7 +157,6 @@ const ApiKeySection = ({ provider, expanded, onExpand, apiKeysData, onUpdateApiK
// Validate Model
const finalModel = model === 'other' ? customModel : model;
if (!isCustomUrl) {
// Only validate model if not using custom URL
if (!finalModel || finalModel.trim() === "") {
setModelError(true);
setModelHelperText("Model is required.");
Expand All @@ -164,6 +165,17 @@ const ApiKeySection = ({ provider, expanded, onExpand, apiKeysData, onUpdateApiK
setModelError(false);
setModelHelperText('');
}
} else {
// For custom URL, validate custom model
if (!finalModel || finalModel.trim() === "") {
setCustomModelError(true);
setCustomModelHelperText("Model is required.");
isValid = false;
} else {
setCustomModelError(false);
setCustomModelHelperText('');
}

}

return isValid;
Expand Down Expand Up @@ -230,6 +242,8 @@ const ApiKeySection = ({ provider, expanded, onExpand, apiKeysData, onUpdateApiK
setBaseUrlHelperText(provider.baseUrlDescription || '');
setModelError(false);
setModelHelperText('');
setCustomModelError(false);
setCustomModelHelperText('');

onUpdateApiKey(provider.id, {
apiKey: '',
Expand Down Expand Up @@ -420,7 +434,13 @@ const ApiKeySection = ({ provider, expanded, onExpand, apiKeysData, onUpdateApiK
placeholder="Enter model name"
sx={inputProps}
value={customModel}
onChange={(e) => setCustomModel(e.target.value)}
onChange={(e) => {
setCustomModel(e.target.value);
setCustomModelError(false);
setCustomModelHelperText('');
}}
error={customModelError}
helperText={customModelHelperText}
/>
</div>
)}
Expand Down
Loading