Commit 2b07302d authored by Your Name's avatar Your Name

Providers for users ok

parent d39326be
......@@ -101,12 +101,14 @@ class ClaudeAuth:
REDIRECT_URI = DEFAULT_REDIRECT_URI
CLI_USER_AGENT = CLI_USER_AGENT
def __init__(self, credentials_file: Optional[str] = None, redirect_uri: Optional[str] = None):
def __init__(self, credentials_file: Optional[str] = None, redirect_uri: Optional[str] = None, skip_initial_load: bool = False, save_callback: Optional[callable] = None):
"""
Initialize Claude authentication.
Args:
credentials_file: Path to credentials file (default: ~/.aisbf/claude_credentials.json)
skip_initial_load: If True, do not load credentials from file on initialization
save_callback: Optional callback to save credentials instead of writing to file
"""
if credentials_file:
self.credentials_file = Path(credentials_file).expanduser()
......@@ -117,6 +119,9 @@ class ClaudeAuth:
# Allow overriding redirect URI for reverse proxy deployments
self.redirect_uri = redirect_uri if redirect_uri is not None else DEFAULT_REDIRECT_URI
self.tokens = None
self._save_callback = save_callback
if not skip_initial_load:
self.tokens = self._load_credentials()
self._oauth_state = None # Store state for OAuth flow
self._code_verifier = None # Store verifier for OAuth flow
......@@ -142,7 +147,25 @@ class ClaudeAuth:
return None
def _save_credentials(self, data: Dict):
"""Save credentials to file with file locking to prevent race conditions."""
"""
Save credentials:
- If save_callback is provided, use it (database save for user providers)
- Otherwise, save to file with file locking to prevent race conditions
"""
self.tokens = data
if self._save_callback:
# User provider: ONLY use callback, NO file fallback EVER
try:
self._save_callback({'tokens': data})
logger.info("ClaudeAuth: Saved credentials via callback")
return
except Exception as e:
logger.error(f"ClaudeAuth: Failed to save credentials to database: {e}")
# DO NOT FALLBACK TO FILE SAVE FOR REGULAR USERS
raise
# Admin/global provider ONLY: save to file
try:
self.tokens = data
# Store id_token if received (contains account info)
......
......@@ -54,13 +54,15 @@ class CodexOAuth2:
Supports authentication with OpenAI's Codex OAuth2 endpoints.
"""
def __init__(self, credentials_file: Optional[str] = None, issuer: Optional[str] = None):
def __init__(self, credentials_file: Optional[str] = None, issuer: Optional[str] = None, skip_initial_load: bool = False, save_callback: Optional[callable] = None):
"""
Initialize Codex OAuth2 client.
Args:
credentials_file: Path to credentials JSON file (default: ~/.aisbf/codex_credentials.json)
issuer: OAuth2 issuer URL (default: https://auth.openai.com)
skip_initial_load: If True, do not load credentials from file on initialization
save_callback: Optional callback to save credentials instead of writing to file
"""
# Expand and resolve path immediately to absolute path
default_path = os.path.expanduser("~/.aisbf/codex_credentials.json")
......@@ -74,6 +76,8 @@ class CodexOAuth2:
self.issuer = (issuer or DEFAULT_ISSUER).rstrip("/")
self.credentials = None
self._save_callback = save_callback
if not skip_initial_load:
self._load_credentials()
def _load_credentials(self) -> None:
......@@ -89,11 +93,27 @@ class CodexOAuth2:
def _save_credentials(self, credentials: Dict[str, Any]) -> None:
"""
Save credentials to file with secure permissions.
Save credentials:
- If save_callback is provided, use it (database save for user providers)
- Otherwise, save to file with secure permissions (admin/global providers)
Args:
credentials: Credentials dict to save
"""
self.credentials = credentials
if self._save_callback:
# User provider: ONLY use callback, NO file fallback EVER
try:
self._save_callback(credentials)
logger.info(f"CodexOAuth2: Saved credentials via callback")
return
except Exception as e:
logger.error(f"CodexOAuth2: Failed to save credentials to database: {e}")
# DO NOT FALLBACK TO FILE SAVE FOR REGULAR USERS
raise
# Admin/global provider ONLY: save to file
try:
# Path is already expanded and absolute from __init__
resolved_path = self.credentials_file
......
......@@ -40,17 +40,20 @@ class KiloOAuth2:
Supports authentication with Kilo Gateway at https://api.kilo.ai.
"""
def __init__(self, credentials_file: Optional[str] = None, api_base: Optional[str] = None):
def __init__(self, credentials_file: Optional[str] = None, api_base: Optional[str] = None, skip_initial_load: bool = False, save_callback: Optional[callable] = None):
"""
Initialize Kilo OAuth2 client.
Args:
credentials_file: Path to credentials JSON file (default: ~/.kilo_credentials.json)
api_base: Base URL for Kilo API (default: https://api.kilo.ai)
skip_initial_load: If True, do not load credentials from file on initialization
"""
self.credentials_file = os.path.expanduser(credentials_file) if credentials_file else os.path.expanduser("~/.kilo_credentials.json")
self.api_base = api_base or os.environ.get("KILO_API_URL", "https://api.kilo.ai")
self.credentials = None
self._save_callback = save_callback
if not skip_initial_load:
self._load_credentials()
def _load_credentials(self) -> None:
......@@ -66,11 +69,27 @@ class KiloOAuth2:
def _save_credentials(self, credentials: Dict[str, Any]) -> None:
"""
Save credentials to file with secure permissions.
Save credentials:
- If save_callback is provided, use it (database save for user providers)
- Otherwise, save to file with secure permissions (admin/global providers)
Args:
credentials: Credentials dict to save
"""
self.credentials = credentials
if self._save_callback:
# User provider: ONLY use callback, NO file fallback EVER
try:
self._save_callback(credentials)
logger.info(f"KiloOAuth2: Saved credentials via callback")
return
except Exception as e:
logger.error(f"KiloOAuth2: Failed to save credentials to database: {e}")
# DO NOT FALLBACK TO FILE SAVE FOR REGULAR USERS
raise
# Admin/global provider ONLY: save to file
try:
# Ensure directory exists
cred_dir = os.path.dirname(self.credentials_file)
......
......@@ -96,7 +96,7 @@ class QwenOAuth2:
Supports authentication with Qwen's OAuth2 endpoints and automatic token refresh.
"""
def __init__(self, credentials_file: Optional[str] = None):
def __init__(self, credentials_file: Optional[str] = None, skip_initial_load: bool = False, save_callback: Optional[callable] = None):
"""
Initialize Qwen OAuth2 client.
......@@ -105,6 +105,8 @@ class QwenOAuth2:
Args:
credentials_file: Path to credentials JSON file (default: ~/.aisbf/qwen_credentials.json)
skip_initial_load: If True, do not load credentials from file on initialization
save_callback: Optional callback to save credentials instead of writing to file
"""
logger.warning(
"⚠️ Qwen OAuth2 service has been discontinued by Qwen. "
......@@ -116,6 +118,8 @@ class QwenOAuth2:
self.credentials = None
self._file_mod_time = 0
self._last_check = 0
self._save_callback = save_callback
if not skip_initial_load:
self._load_credentials()
def _load_credentials(self) -> None:
......@@ -136,11 +140,27 @@ class QwenOAuth2:
def _save_credentials(self, credentials: Dict[str, Any]) -> None:
"""
Save credentials to file with secure permissions and file locking.
Save credentials:
- If save_callback is provided, use it (database save for user providers)
- Otherwise, save to file with secure permissions and file locking (admin/global providers)
Args:
credentials: Credentials dict to save
"""
self.credentials = credentials
if self._save_callback:
# User provider: ONLY use callback, NO file fallback EVER
try:
self._save_callback(credentials)
logger.info(f"QwenOAuth2: Saved credentials via callback")
return
except Exception as e:
logger.error(f"QwenOAuth2: Failed to save credentials to database: {e}")
# DO NOT FALLBACK TO FILE SAVE FOR REGULAR USERS
raise
# Admin/global provider ONLY: save to file
try:
# Ensure directory exists
os.makedirs(os.path.dirname(self.credentials_file), exist_ok=True)
......
......@@ -102,12 +102,21 @@ class ClaudeProviderHandler(BaseProviderHandler):
def _load_auth_from_db(self, provider_id: str, credentials_file: str):
"""
Load OAuth2 credentials from database for non-admin users.
Falls back to file-based credentials if not found in database.
Load OAuth2 credentials:
- Admin users (user_id=None): ONLY load from file
- Regular users: ONLY load from database, NO file fallback
"""
from ..auth.claude import ClaudeAuth
import logging
if self.user_id is None:
# Admin user: ONLY use file-based credentials
logging.getLogger(__name__).info(f"ClaudeProviderHandler: Admin user, loading credentials from file: {credentials_file}")
return ClaudeAuth(credentials_file=credentials_file)
# Regular user: ONLY use database credentials, NO file fallback
try:
from ..database import get_database
from ..auth.claude import ClaudeAuth
db = get_database()
if db:
db_creds = db.get_user_oauth2_credentials(
......@@ -116,22 +125,24 @@ class ClaudeProviderHandler(BaseProviderHandler):
auth_type='claude_oauth2'
)
if db_creds and db_creds.get('credentials'):
# Create auth instance with database credentials
auth = ClaudeAuth(credentials_file=credentials_file)
# Override the loaded credentials with database credentials
# Create auth instance with skip_initial_load=True to avoid file read
# Pass save callback to save credentials back to database
auth = ClaudeAuth(
credentials_file=credentials_file,
skip_initial_load=True,
save_callback=lambda creds: self._save_auth_to_db(creds)
)
# Set tokens directly from database
auth.tokens = db_creds['credentials'].get('tokens', {})
import logging
logging.getLogger(__name__).info(f"ClaudeProviderHandler: Loaded credentials from database for user {self.user_id}")
return auth
except Exception as e:
import logging
logging.getLogger(__name__).warning(f"ClaudeProviderHandler: Failed to load credentials from database: {e}")
# Fall back to file-based credentials
from ..auth.claude import ClaudeAuth
import logging
logging.getLogger(__name__).info(f"ClaudeProviderHandler: Falling back to file-based credentials for user {self.user_id}")
return ClaudeAuth(credentials_file=credentials_file)
# For regular users, NO file fallback - return empty auth instance
logging.getLogger(__name__).info(f"ClaudeProviderHandler: No database credentials found for user {self.user_id}, returning unauthenticated instance")
return ClaudeAuth(credentials_file=credentials_file, skip_initial_load=True)
def _init_session_identifiers(self):
"""Initialize persistent session identifiers (device_id, account_uuid, session_id)."""
......
......@@ -112,9 +112,22 @@ class CodexProviderHandler(BaseProviderHandler):
def _load_oauth2_from_db(self, provider_id: str, credentials_file: str, issuer: str) -> CodexOAuth2:
"""
Load OAuth2 credentials from database for non-admin users.
Falls back to file-based credentials if not found in database.
Load OAuth2 credentials:
- Admin users (user_id=None): ONLY load from file
- Regular users: ONLY load from database, NO file fallback
"""
from ..auth.codex import CodexOAuth2
import logging
if self.user_id is None:
# Admin user: ONLY use file-based credentials
logging.getLogger(__name__).info(f"CodexProviderHandler: Admin user, loading credentials from file: {credentials_file}")
return CodexOAuth2(
credentials_file=credentials_file,
issuer=issuer,
)
# Regular user: ONLY use database credentials, NO file fallback
try:
from ..database import get_database
db = get_database()
......@@ -125,23 +138,28 @@ class CodexProviderHandler(BaseProviderHandler):
auth_type='codex_oauth2'
)
if db_creds and db_creds.get('credentials'):
# Create OAuth2 instance with database credentials
# Create OAuth2 instance with skip_initial_load=True to avoid file read
# Pass save callback to save credentials back to database
oauth2 = CodexOAuth2(
credentials_file=credentials_file,
issuer=issuer,
skip_initial_load=True,
save_callback=lambda creds: self._save_oauth2_to_db(creds)
)
# Override the loaded credentials with database credentials
# Set credentials directly from database
oauth2.credentials = db_creds['credentials']
logger.info(f"CodexProviderHandler: Loaded credentials from database for user {self.user_id}")
logging.getLogger(__name__).info(f"CodexProviderHandler: Loaded credentials from database for user {self.user_id}")
return oauth2
except Exception as e:
logger.warning(f"CodexProviderHandler: Failed to load credentials from database: {e}")
logging.getLogger(__name__).warning(f"CodexProviderHandler: Failed to load credentials from database: {e}")
# Fall back to file-based credentials
logger.info(f"CodexProviderHandler: Falling back to file-based credentials for user {self.user_id}")
# For regular users, NO file fallback - return empty auth instance
logging.getLogger(__name__).info(f"CodexProviderHandler: No database credentials found for user {self.user_id}, returning unauthenticated instance")
return CodexOAuth2(
credentials_file=credentials_file,
issuer=issuer,
skip_initial_load=True,
save_callback=lambda creds: self._save_oauth2_to_db(creds)
)
async def _get_valid_api_key(self) -> str:
......
......@@ -110,12 +110,21 @@ class KiloProviderHandler(BaseProviderHandler):
def _load_oauth2_from_db(self, provider_id: str, credentials_file: str, api_base: str):
"""
Load OAuth2 credentials from database for non-admin users.
Falls back to file-based credentials if not found in database.
Load OAuth2 credentials:
- Admin users (user_id=None): ONLY load from file
- Regular users: ONLY load from database, NO file fallback
"""
from ..auth.kilo import KiloOAuth2
import logging
if self.user_id is None:
# Admin user: ONLY use file-based credentials
logging.getLogger(__name__).info(f"KiloProviderHandler: Admin user, loading credentials from file: {credentials_file}")
return KiloOAuth2(credentials_file=credentials_file, api_base=api_base)
# Regular user: ONLY use database credentials, NO file fallback
try:
from ..database import get_database
from ..auth.kilo import KiloOAuth2
db = get_database()
if db:
db_creds = db.get_user_oauth2_credentials(
......@@ -124,22 +133,29 @@ class KiloProviderHandler(BaseProviderHandler):
auth_type='kilo_oauth2'
)
if db_creds and db_creds.get('credentials'):
# Create OAuth2 instance with database credentials
oauth2 = KiloOAuth2(credentials_file=credentials_file, api_base=api_base)
# Override the loaded credentials with database credentials
# Create OAuth2 instance with skip_initial_load=True to avoid file read
# Pass save callback to save credentials back to database
oauth2 = KiloOAuth2(
credentials_file=credentials_file,
api_base=api_base,
skip_initial_load=True,
save_callback=lambda creds: self._save_oauth2_to_db(creds)
)
# Set credentials directly from database
oauth2.credentials = db_creds['credentials']
import logging
logging.getLogger(__name__).info(f"KiloProviderHandler: Loaded credentials from database for user {self.user_id}")
return oauth2
except Exception as e:
import logging
logging.getLogger(__name__).warning(f"KiloProviderHandler: Failed to load credentials from database: {e}")
# Fall back to file-based credentials
from ..auth.kilo import KiloOAuth2
import logging
logging.getLogger(__name__).info(f"KiloProviderHandler: Falling back to file-based credentials for user {self.user_id}")
return KiloOAuth2(credentials_file=credentials_file, api_base=api_base)
# For regular users, NO file fallback - return empty auth instance
logging.getLogger(__name__).info(f"KiloProviderHandler: No database credentials found for user {self.user_id}, returning unauthenticated instance")
return KiloOAuth2(
credentials_file=credentials_file,
api_base=api_base,
skip_initial_load=True,
save_callback=lambda creds: self._save_oauth2_to_db(creds)
)
def _save_oauth2_to_db(self, credentials: Dict) -> None:
"""
......
......@@ -98,12 +98,21 @@ class QwenProviderHandler(BaseProviderHandler):
def _load_auth_from_db(self, provider_id: str, credentials_file: str):
"""
Load OAuth2 credentials from database for non-admin users.
Falls back to file-based credentials if not found in database.
Load OAuth2 credentials:
- Admin users (user_id=None): ONLY load from file
- Regular users: ONLY load from database, NO file fallback
"""
from ..auth.qwen import QwenOAuth2
import logging
if self.user_id is None:
# Admin user: ONLY use file-based credentials
logging.getLogger(__name__).info(f"QwenProviderHandler: Admin user, loading credentials from file: {credentials_file}")
return QwenOAuth2(credentials_file=credentials_file)
# Regular user: ONLY use database credentials, NO file fallback
try:
from ..database import get_database
from ..auth.qwen import QwenOAuth2
db = get_database()
if db:
db_creds = db.get_user_oauth2_credentials(
......@@ -112,22 +121,52 @@ class QwenProviderHandler(BaseProviderHandler):
auth_type='qwen_oauth2'
)
if db_creds and db_creds.get('credentials'):
# Create auth instance with database credentials
auth = QwenOAuth2(credentials_file=credentials_file)
# Override the loaded credentials with database credentials
# Create auth instance with skip_initial_load=True to avoid file read
# Pass save callback to save credentials back to database
auth = QwenOAuth2(
credentials_file=credentials_file,
skip_initial_load=True,
save_callback=lambda creds: self._save_auth_to_db(creds)
)
# Set credentials directly from database
auth.credentials = db_creds['credentials']
import logging
logging.getLogger(__name__).info(f"QwenProviderHandler: Loaded credentials from database for user {self.user_id}")
return auth
except Exception as e:
import logging
logging.getLogger(__name__).warning(f"QwenProviderHandler: Failed to load credentials from database: {e}")
# Fall back to file-based credentials
from ..auth.qwen import QwenOAuth2
# For regular users, NO file fallback - return empty auth instance
logging.getLogger(__name__).info(f"QwenProviderHandler: No database credentials found for user {self.user_id}, returning unauthenticated instance")
return QwenOAuth2(
credentials_file=credentials_file,
skip_initial_load=True,
save_callback=lambda creds: self._save_auth_to_db(creds)
)
def _save_auth_to_db(self, credentials: Dict) -> None:
"""
Save OAuth2 credentials to database for non-admin users.
This is called after successful device flow authentication.
"""
if self.user_id is None:
# Admin user uses file-based credentials, nothing to save to DB
return
try:
from ..database import get_database
db = get_database()
if db:
db.save_user_oauth2_credentials(
user_id=self.user_id,
provider_id=self.provider_id,
auth_type='qwen_oauth2',
credentials=credentials
)
import logging
logging.getLogger(__name__).info(f"QwenProviderHandler: Falling back to file-based credentials for user {self.user_id}")
return QwenOAuth2(credentials_file=credentials_file)
logging.getLogger(__name__).info(f"QwenProviderHandler: Saved credentials to database for user {self.user_id}")
except Exception as e:
import logging
logging.getLogger(__name__).warning(f"QwenProviderHandler: Failed to save credentials to database: {e}")
async def _get_sdk_client(self):
"""Get or create an OpenAI SDK client configured with authentication (OAuth2 or API key)."""
......
This diff is collapsed.
......@@ -134,6 +134,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
{% block extra_js %}
<script>
let providersData = {};
let expandedProviders = new Set();
let rawProviders = {{ user_providers_json | safe }};
// Convert user providers format to the format expected by the UI
......@@ -141,8 +142,6 @@ rawProviders.forEach(provider => {
providersData[provider.provider_id] = provider.config;
});
let expandedProviders = new Set();
// Chunk size: 512KB chunks for maximum compatibility with restrictive proxies
const CHUNK_SIZE = 512 * 1024;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment