Commit 84d6f6e4 authored by Your Name's avatar Your Name

feat: Integrate existing database module for multi-user support

- Implement user-specific configuration isolation with SQLite database
- Add user management, authentication, and role-based access control
- Create user-specific providers, rotations, and autoselect configurations
- Add API token management and usage tracking per user
- Update handlers to support user-specific configs with fallback to global
- Add MCP support for user-specific configurations
- Update documentation and README with multi-user features
- Add user dashboard templates for configuration management
parent 4ec3cf51
...@@ -297,15 +297,18 @@ AISBF includes a comprehensive SQLite database system that provides persistent t ...@@ -297,15 +297,18 @@ AISBF includes a comprehensive SQLite database system that provides persistent t
The database (`~/.aisbf/aisbf.db`) contains the following tables: The database (`~/.aisbf/aisbf.db`) contains the following tables:
#### Core Tracking Tables
- **`context_dimensions`**: Tracks context size, condensation settings, and effective context per model - **`context_dimensions`**: Tracks context size, condensation settings, and effective context per model
- **`token_usage`**: Persistent token usage tracking with TPM/TPH/TPD rate limiting across restarts - **`token_usage`**: Persistent token usage tracking with TPM/TPH/TPD rate limiting across restarts
- **`model_embeddings`**: Caches model embeddings for semantic classification performance - **`model_embeddings`**: Caches model embeddings for semantic classification performance
#### Multi-User Tables
- **`users`**: User management with authentication, roles (admin/user), and metadata - **`users`**: User management with authentication, roles (admin/user), and metadata
- **`user_providers`**: Isolated provider configurations per user - **`user_providers`**: Isolated provider configurations per user (JSON stored configurations)
- **`user_rotations`**: Isolated rotation configurations per user - **`user_rotations`**: Isolated rotation configurations per user (JSON stored configurations)
- **`user_autoselects`**: Isolated autoselect configurations per user - **`user_autoselects`**: Isolated autoselect configurations per user (JSON stored configurations)
- **`user_api_tokens`**: API token management per user for MCP and API access - **`user_api_tokens`**: API token management per user for MCP and API access
- **`user_token_usage`**: Per-user token usage tracking - **`user_token_usage`**: Per-user token usage tracking and analytics
### Database Initialization ### Database Initialization
...@@ -317,31 +320,56 @@ The database is automatically initialized on startup: ...@@ -317,31 +320,56 @@ The database is automatically initialized on startup:
### Multi-User Support ### Multi-User Support
AISBF supports multiple users with complete isolation: AISBF supports multiple users with complete isolation through database-backed configurations:
#### User Authentication System
- **Database-First Authentication**: User credentials stored in SQLite database with SHA256 password hashing
- **Config Admin Fallback**: Legacy config-based admin authentication for backward compatibility
- **Role-Based Access Control**: Two roles - `admin` (full access) and `user` (isolated access)
- **Session-Based Authentication**: Secure session management for dashboard access
#### User Isolation Architecture
Each user has completely isolated configurations stored as JSON in the database:
- **Provider Configurations**: `user_providers` table stores individual API keys, endpoints, and model settings
- **Rotation Configurations**: `user_rotations` table stores personal load balancing rules
- **Autoselect Configurations**: `user_autoselects` table stores custom AI-assisted model selection rules
- **API Tokens**: `user_api_tokens` table manages multiple API tokens per user
- **Usage Tracking**: `user_token_usage` table provides per-user analytics and rate limiting
#### Handler Architecture
The system uses user-specific handler instances that load configurations from the database:
```python
# Handler instantiation with user context
request_handler = RequestHandler(user_id=user_id)
rotation_handler = RotationHandler(user_id=user_id)
autoselect_handler = AutoselectHandler(user_id=user_id)
# Automatic loading of user configs
handler.user_providers = db.get_user_providers(user_id)
handler.user_rotations = db.get_user_rotations(user_id)
handler.user_autoselects = db.get_user_autoselects(user_id)
```
#### User Authentication #### Configuration Priority
- Database-first authentication with config admin fallback When processing requests, handlers check for user-specific configurations first:
- SHA256 password hashing for security
- Role-based access control (admin vs user roles)
- Session-based authentication
#### User Isolation 1. **User-specific configs** (from database)
- Each user has isolated provider, rotation, and autoselect configurations 2. **Global configs** (from JSON files)
- Separate API tokens per user 3. **System defaults**
- Individual token usage tracking
- User-specific dashboard access
#### Admin Features #### Admin Features
- Create/manage users via database - Create and manage users via database
- Full system configuration access - Full access to global configurations and user management
- User management dashboard (future feature)
- System-wide analytics and monitoring - System-wide analytics and monitoring
- User administration dashboard
#### User Dashboard #### User Dashboard Features
- Usage statistics and token tracking - **Personal Configuration Management**: Create/edit/delete provider, rotation, and autoselect configs
- Personal configuration management - **Usage Statistics**: Real-time token usage tracking and analytics
- API token generation and management - **API Token Management**: Generate, view, and delete API tokens
- Restricted access to system settings - **Isolated Access**: No visibility of other users' configurations or system settings
### Persistent Tracking ### Persistent Tracking
......
...@@ -40,6 +40,8 @@ Access the dashboard at `http://localhost:17765/dashboard` (default credentials: ...@@ -40,6 +40,8 @@ Access the dashboard at `http://localhost:17765/dashboard` (default credentials:
- **MCP Server**: Model Context Protocol server for remote agent configuration and model access (SSE and HTTP streaming) - **MCP Server**: Model Context Protocol server for remote agent configuration and model access (SSE and HTTP streaming)
- **Persistent Database**: SQLite-based tracking of token usage, context dimensions, and model embeddings with automatic cleanup - **Persistent Database**: SQLite-based tracking of token usage, context dimensions, and model embeddings with automatic cleanup
- **Multi-User Support**: User management with isolated configurations, role-based access control, and API token management - **Multi-User Support**: User management with isolated configurations, role-based access control, and API token management
- **Database Integration**: SQLite-based persistent storage for user configurations, token usage tracking, and context management
- **User-Specific Configurations**: Each user can have their own providers, rotations, and autoselect configurations stored in the database
## Author ## Author
...@@ -293,6 +295,33 @@ When context exceeds the configured percentage of `context_size`, the system aut ...@@ -293,6 +295,33 @@ When context exceeds the configured percentage of `context_size`, the system aut
See `config/providers.json` and `config/rotations.json` for configuration examples. See `config/providers.json` and `config/rotations.json` for configuration examples.
### Multi-User Database Integration
AISBF includes comprehensive multi-user support with isolated configurations stored in a SQLite database:
#### User Management
- **Admin Users**: Full access to global configurations and user management
- **Regular Users**: Access to their own configurations and usage statistics
- **Role-Based Access**: Secure separation between admin and user roles
#### Database Features
- **Persistent Storage**: All configurations stored in SQLite database with automatic initialization
- **Token Usage Tracking**: Per-user API token usage statistics and analytics
- **Configuration Isolation**: Each user has separate providers, rotations, and autoselect configurations
- **Automatic Cleanup**: Database maintenance with configurable retention periods
#### User-Specific Configurations
Users can create and manage their own:
- **Providers**: Custom API endpoints, models, and authentication settings
- **Rotations**: Personal load balancing configurations across providers
- **Autoselect**: Custom AI-powered model selection rules
- **API Tokens**: Multiple API tokens with usage tracking and management
#### Dashboard Access
- **Admin Dashboard**: Global configuration management and user administration
- **User Dashboard**: Personal configuration management and usage statistics
- **API Token Management**: Create, view, and delete API tokens with usage analytics
### Content Classification and Semantic Selection ### Content Classification and Semantic Selection
AISBF provides advanced content filtering and intelligent model selection based on content analysis: AISBF provides advanced content filtering and intelligent model selection based on content analysis:
......
...@@ -442,6 +442,537 @@ class DatabaseManager: ...@@ -442,6 +442,537 @@ class DatabaseManager:
'TPD': self.get_token_usage(provider_id, model_name, '1d') 'TPD': self.get_token_usage(provider_id, model_name, '1d')
} }
# User management methods
def authenticate_user(self, username: str, password_hash: str) -> Optional[Dict]:
"""
Authenticate a user by username and password hash.
Args:
username: Username to authenticate
password_hash: SHA256 hash of the password
Returns:
User dict if authenticated, None otherwise
"""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute('''
SELECT id, username, role, is_active
FROM users
WHERE username = ? AND password_hash = ? AND is_active = 1
''', (username, password_hash))
row = cursor.fetchone()
if row:
return {
'id': row[0],
'username': row[1],
'role': row[2],
'is_active': row[3]
}
return None
def create_user(self, username: str, password_hash: str, role: str = 'user', created_by: str = None) -> int:
"""
Create a new user.
Args:
username: Username for the new user
password_hash: SHA256 hash of the password
role: User role ('admin' or 'user')
created_by: Username of the creator
Returns:
User ID of the created user
"""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute('''
INSERT INTO users (username, password_hash, role, created_by)
VALUES (?, ?, ?, ?)
''', (username, password_hash, role, created_by))
conn.commit()
return cursor.lastrowid
def get_users(self) -> List[Dict]:
"""
Get all users.
Returns:
List of user dictionaries
"""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute('''
SELECT id, username, role, created_by, created_at, last_login, is_active
FROM users
ORDER BY created_at DESC
''')
users = []
for row in cursor.fetchall():
users.append({
'id': row[0],
'username': row[1],
'role': row[2],
'created_by': row[3],
'created_at': row[4],
'last_login': row[5],
'is_active': row[6]
})
return users
def delete_user(self, user_id: int):
"""
Delete a user and all their configurations.
Args:
user_id: User ID to delete
"""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
# Delete user configurations first (due to foreign key constraints)
cursor.execute('DELETE FROM user_providers WHERE user_id = ?', (user_id,))
cursor.execute('DELETE FROM user_rotations WHERE user_id = ?', (user_id,))
cursor.execute('DELETE FROM user_autoselects WHERE user_id = ?', (user_id,))
cursor.execute('DELETE FROM user_api_tokens WHERE user_id = ?', (user_id,))
cursor.execute('DELETE FROM user_token_usage WHERE user_id = ?', (user_id,))
# Delete the user
cursor.execute('DELETE FROM users WHERE id = ?', (user_id,))
conn.commit()
# User-specific provider methods
def save_user_provider(self, user_id: int, provider_name: str, config: Dict):
"""
Save user-specific provider configuration.
Args:
user_id: User ID
provider_name: Provider name
config: Provider configuration dictionary
"""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
config_json = json.dumps(config)
cursor.execute('''
INSERT OR REPLACE INTO user_providers (user_id, provider_id, config, updated_at)
VALUES (?, ?, ?, CURRENT_TIMESTAMP)
''', (user_id, provider_name, config_json))
conn.commit()
def get_user_providers(self, user_id: int) -> List[Dict]:
"""
Get all user-specific providers for a user.
Args:
user_id: User ID
Returns:
List of provider configurations
"""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute('''
SELECT provider_id, config, created_at, updated_at
FROM user_providers
WHERE user_id = ?
ORDER BY provider_id
''', (user_id,))
providers = []
for row in cursor.fetchall():
providers.append({
'provider_id': row[0],
'config': json.loads(row[1]),
'created_at': row[2],
'updated_at': row[3]
})
return providers
def get_user_provider(self, user_id: int, provider_name: str) -> Optional[Dict]:
"""
Get a specific user provider configuration.
Args:
user_id: User ID
provider_name: Provider name
Returns:
Provider configuration dict or None
"""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute('''
SELECT config, created_at, updated_at
FROM user_providers
WHERE user_id = ? AND provider_id = ?
''', (user_id, provider_name))
row = cursor.fetchone()
if row:
return {
'config': json.loads(row[0]),
'created_at': row[1],
'updated_at': row[2]
}
return None
def delete_user_provider(self, user_id: int, provider_name: str):
"""
Delete a user-specific provider configuration.
Args:
user_id: User ID
provider_name: Provider name
"""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute('''
DELETE FROM user_providers
WHERE user_id = ? AND provider_id = ?
''', (user_id, provider_name))
conn.commit()
# User-specific rotation methods
def save_user_rotation(self, user_id: int, rotation_name: str, config: Dict):
"""
Save user-specific rotation configuration.
Args:
user_id: User ID
rotation_name: Rotation name
config: Rotation configuration dictionary
"""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
config_json = json.dumps(config)
cursor.execute('''
INSERT OR REPLACE INTO user_rotations (user_id, rotation_id, config, updated_at)
VALUES (?, ?, ?, CURRENT_TIMESTAMP)
''', (user_id, rotation_name, config_json))
conn.commit()
def get_user_rotations(self, user_id: int) -> List[Dict]:
"""
Get all user-specific rotations for a user.
Args:
user_id: User ID
Returns:
List of rotation configurations
"""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute('''
SELECT rotation_id, config, created_at, updated_at
FROM user_rotations
WHERE user_id = ?
ORDER BY rotation_id
''', (user_id,))
rotations = []
for row in cursor.fetchall():
rotations.append({
'rotation_id': row[0],
'config': json.loads(row[1]),
'created_at': row[2],
'updated_at': row[3]
})
return rotations
def get_user_rotation(self, user_id: int, rotation_name: str) -> Optional[Dict]:
"""
Get a specific user rotation configuration.
Args:
user_id: User ID
rotation_name: Rotation name
Returns:
Rotation configuration dict or None
"""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute('''
SELECT config, created_at, updated_at
FROM user_rotations
WHERE user_id = ? AND rotation_id = ?
''', (user_id, rotation_name))
row = cursor.fetchone()
if row:
return {
'config': json.loads(row[0]),
'created_at': row[1],
'updated_at': row[2]
}
return None
def delete_user_rotation(self, user_id: int, rotation_name: str):
"""
Delete a user-specific rotation configuration.
Args:
user_id: User ID
rotation_name: Rotation name
"""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute('''
DELETE FROM user_rotations
WHERE user_id = ? AND rotation_id = ?
''', (user_id, rotation_name))
conn.commit()
# User-specific autoselect methods
def save_user_autoselect(self, user_id: int, autoselect_name: str, config: Dict):
"""
Save user-specific autoselect configuration.
Args:
user_id: User ID
autoselect_name: Autoselect name
config: Autoselect configuration dictionary
"""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
config_json = json.dumps(config)
cursor.execute('''
INSERT OR REPLACE INTO user_autoselects (user_id, autoselect_id, config, updated_at)
VALUES (?, ?, ?, CURRENT_TIMESTAMP)
''', (user_id, autoselect_name, config_json))
conn.commit()
def get_user_autoselects(self, user_id: int) -> List[Dict]:
"""
Get all user-specific autoselects for a user.
Args:
user_id: User ID
Returns:
List of autoselect configurations
"""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute('''
SELECT autoselect_id, config, created_at, updated_at
FROM user_autoselects
WHERE user_id = ?
ORDER BY autoselect_id
''', (user_id,))
autoselects = []
for row in cursor.fetchall():
autoselects.append({
'autoselect_id': row[0],
'config': json.loads(row[1]),
'created_at': row[2],
'updated_at': row[3]
})
return autoselects
def get_user_autoselect(self, user_id: int, autoselect_name: str) -> Optional[Dict]:
"""
Get a specific user autoselect configuration.
Args:
user_id: User ID
autoselect_name: Autoselect name
Returns:
Autoselect configuration dict or None
"""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute('''
SELECT config, created_at, updated_at
FROM user_autoselects
WHERE user_id = ? AND autoselect_id = ?
''', (user_id, autoselect_name))
row = cursor.fetchone()
if row:
return {
'config': json.loads(row[0]),
'created_at': row[1],
'updated_at': row[2]
}
return None
def delete_user_autoselect(self, user_id: int, autoselect_name: str):
"""
Delete a user-specific autoselect configuration.
Args:
user_id: User ID
autoselect_name: Autoselect name
"""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute('''
DELETE FROM user_autoselects
WHERE user_id = ? AND autoselect_id = ?
''', (user_id, autoselect_name))
conn.commit()
# User API token methods
def create_user_api_token(self, user_id: int, token: str, description: str = None) -> int:
"""
Create a new API token for a user.
Args:
user_id: User ID
token: The token string
description: Optional description
Returns:
Token ID
"""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute('''
INSERT INTO user_api_tokens (user_id, token, description)
VALUES (?, ?, ?)
''', (user_id, token, description))
conn.commit()
return cursor.lastrowid
def get_user_api_tokens(self, user_id: int) -> List[Dict]:
"""
Get all API tokens for a user.
Args:
user_id: User ID
Returns:
List of token dictionaries
"""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute('''
SELECT id, token, description, created_at, last_used, is_active
FROM user_api_tokens
WHERE user_id = ?
ORDER BY created_at DESC
''', (user_id,))
tokens = []
for row in cursor.fetchall():
tokens.append({
'id': row[0],
'token': row[1],
'description': row[2],
'created_at': row[3],
'last_used': row[4],
'is_active': row[5]
})
return tokens
def authenticate_user_token(self, token: str) -> Optional[Dict]:
"""
Authenticate a user by API token.
Args:
token: API token string
Returns:
User dict if authenticated, None otherwise
"""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute('''
SELECT u.id, u.username, u.role, t.id as token_id
FROM users u
JOIN user_api_tokens t ON u.id = t.user_id
WHERE t.token = ? AND t.is_active = 1 AND u.is_active = 1
''', (token,))
row = cursor.fetchone()
if row:
return {
'user_id': row[0],
'username': row[1],
'role': row[2],
'token_id': row[3]
}
return None
def delete_user_api_token(self, user_id: int, token_id: int):
"""
Delete a user API token.
Args:
user_id: User ID
token_id: Token ID
"""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute('''
DELETE FROM user_api_tokens
WHERE id = ? AND user_id = ?
''', (token_id, user_id))
conn.commit()
# User token usage methods
def record_user_token_usage(self, user_id: int, token_id: int, provider_id: str, model_name: str, tokens_used: int):
"""
Record token usage for a user API request.
Args:
user_id: User ID
token_id: API token ID
provider_id: Provider identifier
model_name: Model name
tokens_used: Number of tokens used
"""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute('''
INSERT INTO user_token_usage (user_id, token_id, provider_id, model_name, tokens_used)
VALUES (?, ?, ?, ?, ?)
''', (user_id, token_id, provider_id, model_name, tokens_used))
# Update last_used timestamp for the token
cursor.execute('''
UPDATE user_api_tokens
SET last_used = CURRENT_TIMESTAMP
WHERE id = ?
''', (token_id,))
conn.commit()
def get_user_token_usage(self, user_id: int) -> List[Dict]:
"""
Get token usage for a user.
Args:
user_id: User ID
Returns:
List of token usage records
"""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute('''
SELECT provider_id, model_name, tokens_used, timestamp
FROM user_token_usage
WHERE user_id = ?
ORDER BY timestamp DESC
LIMIT 1000
''', (user_id,))
usage = []
for row in cursor.fetchall():
usage.append({
'provider_id': row[0],
'model_name': row[1],
'token_count': row[2],
'timestamp': row[3]
})
return usage
# Global database manager instance # Global database manager instance
_db_manager: Optional[DatabaseManager] = None _db_manager: Optional[DatabaseManager] = None
......
...@@ -74,8 +74,24 @@ def generate_system_fingerprint(provider_id: str, seed: Optional[int] = None) -> ...@@ -74,8 +74,24 @@ def generate_system_fingerprint(provider_id: str, seed: Optional[int] = None) ->
return f"fp_{hash_value}" return f"fp_{hash_value}"
class RequestHandler: class RequestHandler:
def __init__(self): def __init__(self, user_id=None):
self.user_id = user_id
self.config = config self.config = config
# Load user-specific configs if user_id is provided
if user_id:
self._load_user_configs()
else:
self.user_providers = {}
self.user_rotations = {}
self.user_autoselects = {}
def _load_user_configs(self):
"""Load user-specific configurations from database"""
from .database import get_database
db = get_database()
self.user_providers = db.get_user_providers(self.user_id)
self.user_rotations = db.get_user_rotations(self.user_id)
self.user_autoselects = db.get_user_autoselects(self.user_id)
async def _handle_chunked_request( async def _handle_chunked_request(
self, self,
...@@ -210,9 +226,17 @@ class RequestHandler: ...@@ -210,9 +226,17 @@ class RequestHandler:
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
logger.info(f"=== RequestHandler.handle_chat_completion START ===") logger.info(f"=== RequestHandler.handle_chat_completion START ===")
logger.info(f"Provider ID: {provider_id}") logger.info(f"Provider ID: {provider_id}")
logger.info(f"User ID: {self.user_id}")
logger.info(f"Request data: {request_data}") logger.info(f"Request data: {request_data}")
provider_config = self.config.get_provider(provider_id) # Check for user-specific provider config first
if self.user_id and provider_id in self.user_providers:
provider_config = self.user_providers[provider_id]
logger.info(f"Using user-specific provider config for {provider_id}")
else:
provider_config = self.config.get_provider(provider_id)
logger.info(f"Using global provider config for {provider_id}")
logger.info(f"Provider config: {provider_config}") logger.info(f"Provider config: {provider_config}")
logger.info(f"Provider type: {provider_config.type}") logger.info(f"Provider type: {provider_config.type}")
logger.info(f"Provider endpoint: {provider_config.endpoint}") logger.info(f"Provider endpoint: {provider_config.endpoint}")
...@@ -338,7 +362,11 @@ class RequestHandler: ...@@ -338,7 +362,11 @@ class RequestHandler:
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
async def handle_streaming_chat_completion(self, request: Request, provider_id: str, request_data: Dict): async def handle_streaming_chat_completion(self, request: Request, provider_id: str, request_data: Dict):
provider_config = self.config.get_provider(provider_id) # Check for user-specific provider config first
if self.user_id and provider_id in self.user_providers:
provider_config = self.user_providers[provider_id]
else:
provider_config = self.config.get_provider(provider_id)
if provider_config.api_key_required: if provider_config.api_key_required:
api_key = request_data.get('api_key') or request.headers.get('Authorization', '').replace('Bearer ', '') api_key = request_data.get('api_key') or request.headers.get('Authorization', '').replace('Bearer ', '')
...@@ -1214,8 +1242,24 @@ class RequestHandler: ...@@ -1214,8 +1242,24 @@ class RequestHandler:
raise HTTPException(status_code=500, detail=f"Error fetching content: {str(e)}") raise HTTPException(status_code=500, detail=f"Error fetching content: {str(e)}")
class RotationHandler: class RotationHandler:
def __init__(self): def __init__(self, user_id=None):
self.user_id = user_id
self.config = config self.config = config
# Load user-specific configs if user_id is provided
if user_id:
self._load_user_configs()
else:
self.user_providers = {}
self.user_rotations = {}
self.user_autoselects = {}
def _load_user_configs(self):
"""Load user-specific configurations from database"""
from .database import get_database
db = get_database()
self.user_providers = db.get_user_providers(self.user_id)
self.user_rotations = db.get_user_rotations(self.user_id)
self.user_autoselects = db.get_user_autoselects(self.user_id)
def _get_provider_type(self, provider_id: str) -> str: def _get_provider_type(self, provider_id: str) -> str:
"""Get the provider type from configuration""" """Get the provider type from configuration"""
...@@ -1435,7 +1479,7 @@ class RotationHandler: ...@@ -1435,7 +1479,7 @@ class RotationHandler:
async def handle_rotation_request(self, rotation_id: str, request_data: Dict): async def handle_rotation_request(self, rotation_id: str, request_data: Dict):
""" """
Handle a rotation request. Handle a rotation request.
For streaming requests, returns a StreamingResponse with proper handling For streaming requests, returns a StreamingResponse with proper handling
based on the selected provider's type (google vs others). based on the selected provider's type (google vs others).
For non-streaming requests, returns the response dict directly. For non-streaming requests, returns the response dict directly.
...@@ -1445,8 +1489,16 @@ class RotationHandler: ...@@ -1445,8 +1489,16 @@ class RotationHandler:
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
logger.info(f"=== RotationHandler.handle_rotation_request START ===") logger.info(f"=== RotationHandler.handle_rotation_request START ===")
logger.info(f"Rotation ID: {rotation_id}") logger.info(f"Rotation ID: {rotation_id}")
logger.info(f"User ID: {self.user_id}")
rotation_config = self.config.get_rotation(rotation_id)
# Check for user-specific rotation config first
if self.user_id and rotation_id in self.user_rotations:
rotation_config = self.user_rotations[rotation_id]
logger.info(f"Using user-specific rotation config for {rotation_id}")
else:
rotation_config = self.config.get_rotation(rotation_id)
logger.info(f"Using global rotation config for {rotation_id}")
if not rotation_config: if not rotation_config:
logger.error(f"Rotation {rotation_id} not found") logger.error(f"Rotation {rotation_id} not found")
raise HTTPException(status_code=400, detail=f"Rotation {rotation_id} not found") raise HTTPException(status_code=400, detail=f"Rotation {rotation_id} not found")
...@@ -1476,11 +1528,17 @@ class RotationHandler: ...@@ -1476,11 +1528,17 @@ class RotationHandler:
logger.info(f"") logger.info(f"")
logger.info(f"--- Processing provider: {provider_id} ---") logger.info(f"--- Processing provider: {provider_id} ---")
# Check if provider exists in configuration # Check if provider exists in configuration (user-specific first, then global)
provider_config = self.config.get_provider(provider_id) if self.user_id and provider_id in self.user_providers:
provider_config = self.user_providers[provider_id]
logger.info(f" [USER] Using user-specific provider config for {provider_id}")
else:
provider_config = self.config.get_provider(provider_id)
logger.info(f" [GLOBAL] Using global provider config for {provider_id}")
if not provider_config: if not provider_config:
logger.error(f" [ERROR] Provider {provider_id} not found in providers configuration") logger.error(f" [ERROR] Provider {provider_id} not found in providers configuration")
logger.error(f" Available providers: {list(self.config.providers.keys())}") logger.error(f" Available providers: {list(self.config.providers.keys()) if not self.user_id else list(self.user_providers.keys())}")
logger.error(f" Skipping this provider") logger.error(f" Skipping this provider")
skipped_providers.append(provider_id) skipped_providers.append(provider_id)
continue continue
...@@ -2760,12 +2818,28 @@ class RotationHandler: ...@@ -2760,12 +2818,28 @@ class RotationHandler:
return capabilities return capabilities
class AutoselectHandler: class AutoselectHandler:
def __init__(self): def __init__(self, user_id=None):
self.user_id = user_id
self.config = config self.config = config
self._skill_file_content = None self._skill_file_content = None
self._internal_model = None self._internal_model = None
self._internal_tokenizer = None self._internal_tokenizer = None
self._internal_model_lock = None self._internal_model_lock = None
# Load user-specific configs if user_id is provided
if user_id:
self._load_user_configs()
else:
self.user_providers = {}
self.user_rotations = {}
self.user_autoselects = {}
def _load_user_configs(self):
"""Load user-specific configurations from database"""
from .database import get_database
db = get_database()
self.user_providers = db.get_user_providers(self.user_id)
self.user_rotations = db.get_user_rotations(self.user_id)
self.user_autoselects = db.get_user_autoselects(self.user_id)
def _get_skill_file_content(self) -> str: def _get_skill_file_content(self) -> str:
"""Load the autoselect.md skill file content""" """Load the autoselect.md skill file content"""
...@@ -3130,8 +3204,16 @@ class AutoselectHandler: ...@@ -3130,8 +3204,16 @@ class AutoselectHandler:
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
logger.info(f"=== AUTOSELECT REQUEST START ===") logger.info(f"=== AUTOSELECT REQUEST START ===")
logger.info(f"Autoselect ID: {autoselect_id}") logger.info(f"Autoselect ID: {autoselect_id}")
logger.info(f"User ID: {self.user_id}")
autoselect_config = self.config.get_autoselect(autoselect_id)
# Check for user-specific autoselect config first
if self.user_id and autoselect_id in self.user_autoselects:
autoselect_config = self.user_autoselects[autoselect_id]
logger.info(f"Using user-specific autoselect config for {autoselect_id}")
else:
autoselect_config = self.config.get_autoselect(autoselect_id)
logger.info(f"Using global autoselect config for {autoselect_id}")
if not autoselect_config: if not autoselect_config:
logger.error(f"Autoselect {autoselect_id} not found") logger.error(f"Autoselect {autoselect_id} not found")
raise HTTPException(status_code=400, detail=f"Autoselect {autoselect_id} not found") raise HTTPException(status_code=400, detail=f"Autoselect {autoselect_id} not found")
......
...@@ -427,7 +427,7 @@ class MCPServer: ...@@ -427,7 +427,7 @@ class MCPServer:
return tools return tools
async def handle_tool_call(self, tool_name: str, arguments: Dict, auth_level: int) -> Dict: async def handle_tool_call(self, tool_name: str, arguments: Dict, auth_level: int, user_id: Optional[int] = None) -> Dict:
""" """
Handle an MCP tool call. Handle an MCP tool call.
...@@ -476,7 +476,7 @@ class MCPServer: ...@@ -476,7 +476,7 @@ class MCPServer:
raise HTTPException(status_code=404, detail=f"Tool '{tool_name}' not found") raise HTTPException(status_code=404, detail=f"Tool '{tool_name}' not found")
handler = handlers[tool_name] handler = handlers[tool_name]
return await handler(arguments) return await handler(arguments, user_id)
async def _list_models(self, args: Dict) -> Dict: async def _list_models(self, args: Dict) -> Dict:
"""List all available models""" """List all available models"""
...@@ -562,18 +562,18 @@ class MCPServer: ...@@ -562,18 +562,18 @@ class MCPServer:
} }
return {"autoselect": autoselect_info} return {"autoselect": autoselect_info}
async def _chat_completion(self, args: Dict) -> Dict: async def _chat_completion(self, args: Dict, user_id: Optional[int] = None) -> Dict:
"""Handle chat completion request""" """Handle chat completion request"""
from .handlers import RequestHandler, RotationHandler, AutoselectHandler from .handlers import RequestHandler, RotationHandler, AutoselectHandler
from .models import ChatCompletionRequest from .models import ChatCompletionRequest
from starlette.requests import Request from starlette.requests import Request
model = args.get('model') model = args.get('model')
messages = args.get('messages', []) messages = args.get('messages', [])
temperature = args.get('temperature', 1.0) temperature = args.get('temperature', 1.0)
max_tokens = args.get('max_tokens', 2048) max_tokens = args.get('max_tokens', 2048)
stream = args.get('stream', False) stream = args.get('stream', False)
# Parse provider from model # Parse provider from model
if '/' in model: if '/' in model:
parts = model.split('/', 1) parts = model.split('/', 1)
...@@ -582,7 +582,7 @@ class MCPServer: ...@@ -582,7 +582,7 @@ class MCPServer:
else: else:
provider_id = model provider_id = model
actual_model = model actual_model = model
# Create request data # Create request data
request_data = { request_data = {
"model": actual_model, "model": actual_model,
...@@ -591,7 +591,7 @@ class MCPServer: ...@@ -591,7 +591,7 @@ class MCPServer:
"max_tokens": max_tokens, "max_tokens": max_tokens,
"stream": stream "stream": stream
} }
# Create dummy request # Create dummy request
scope = { scope = {
"type": "http", "type": "http",
...@@ -601,25 +601,26 @@ class MCPServer: ...@@ -601,25 +601,26 @@ class MCPServer:
"path": f"/api/{provider_id}/chat/completions" "path": f"/api/{provider_id}/chat/completions"
} }
dummy_request = Request(scope) dummy_request = Request(scope)
# Route to appropriate handler # Route to appropriate handler (with user_id support)
from main import get_user_handler
if provider_id == "autoselect": if provider_id == "autoselect":
if actual_model not in self.config.autoselect: handler = get_user_handler('autoselect', user_id)
if actual_model not in self.config.autoselect and (not user_id or actual_model not in handler.user_autoselects):
raise HTTPException(status_code=400, detail=f"Autoselect '{actual_model}' not found") raise HTTPException(status_code=400, detail=f"Autoselect '{actual_model}' not found")
handler = AutoselectHandler()
if stream: if stream:
return {"error": "Streaming not supported in MCP, use SSE endpoint instead"} return {"error": "Streaming not supported in MCP, use SSE endpoint instead"}
else: else:
return await handler.handle_autoselect_request(actual_model, request_data) return await handler.handle_autoselect_request(actual_model, request_data)
elif provider_id == "rotation": elif provider_id == "rotation":
if actual_model not in self.config.rotations: handler = get_user_handler('rotation', user_id)
if actual_model not in self.config.rotations and (not user_id or actual_model not in handler.user_rotations):
raise HTTPException(status_code=400, detail=f"Rotation '{actual_model}' not found") raise HTTPException(status_code=400, detail=f"Rotation '{actual_model}' not found")
handler = RotationHandler()
return await handler.handle_rotation_request(actual_model, request_data) return await handler.handle_rotation_request(actual_model, request_data)
else: else:
if provider_id not in self.config.providers: handler = get_user_handler('request', user_id)
if provider_id not in self.config.providers and (not user_id or provider_id not in handler.user_providers):
raise HTTPException(status_code=400, detail=f"Provider '{provider_id}' not found") raise HTTPException(status_code=400, detail=f"Provider '{provider_id}' not found")
handler = RequestHandler()
if stream: if stream:
return {"error": "Streaming not supported in MCP, use SSE endpoint instead"} return {"error": "Streaming not supported in MCP, use SSE endpoint instead"}
else: else:
......
...@@ -481,6 +481,43 @@ autoselect_handler = None ...@@ -481,6 +481,43 @@ autoselect_handler = None
server_config = None server_config = None
config = None config = None
_initialized = False _initialized = False
# Cache for user-specific handlers to avoid recreating them
_user_handlers_cache = {}
def get_user_handler(handler_type: str, user_id=None):
"""Get the appropriate handler for a user, with caching"""
global request_handler, rotation_handler, autoselect_handler, _user_handlers_cache
if user_id is None:
# Return global handlers for non-authenticated requests
if handler_type == 'request':
return request_handler
elif handler_type == 'rotation':
return rotation_handler
elif handler_type == 'autoselect':
return autoselect_handler
else:
raise ValueError(f"Unknown handler type: {handler_type}")
# Check cache first
cache_key = f"{handler_type}_{user_id}"
if cache_key in _user_handlers_cache:
return _user_handlers_cache[cache_key]
# Create new handler instance for this user
if handler_type == 'request':
handler = RequestHandler(user_id)
elif handler_type == 'rotation':
handler = RotationHandler(user_id)
elif handler_type == 'autoselect':
handler = AutoselectHandler(user_id)
else:
raise ValueError(f"Unknown handler type: {handler_type}")
# Cache it
_user_handlers_cache[cache_key] = handler
return handler
tor_service = None tor_service = None
# Model cache for dynamically fetched provider models # Model cache for dynamically fetched provider models
...@@ -560,7 +597,7 @@ async def fetch_provider_models(provider_id: str) -> list: ...@@ -560,7 +597,7 @@ async def fetch_provider_models(provider_id: str) -> list:
} }
dummy_request = Request(scope) dummy_request = Request(scope)
# Fetch models from provider API # Fetch models from provider API (use global handler for model fetching)
models = await request_handler.handle_model_list(dummy_request, provider_id) models = await request_handler.handle_model_list(dummy_request, provider_id)
# Cache the results # Cache the results
...@@ -917,33 +954,70 @@ async def auth_middleware(request: Request, call_next): ...@@ -917,33 +954,70 @@ async def auth_middleware(request: Request, call_next):
if request.url.path == "/" or request.url.path.startswith("/dashboard"): if request.url.path == "/" or request.url.path.startswith("/dashboard"):
response = await call_next(request) response = await call_next(request)
return response return response
# Skip auth for public models endpoints (GET only) # Skip auth for public models endpoints (GET only)
if request.method == "GET" and request.url.path in ["/api/models", "/api/v1/models"]: if request.method == "GET" and request.url.path in ["/api/models", "/api/v1/models"]:
response = await call_next(request) response = await call_next(request)
return response return response
# Check for Authorization header # Check for Authorization header
auth_header = request.headers.get('Authorization', '') auth_header = request.headers.get('Authorization', '')
if not auth_header.startswith('Bearer '): if not auth_header.startswith('Bearer '):
return JSONResponse( return JSONResponse(
status_code=401, status_code=401,
content={"error": "Missing or invalid Authorization header. Use: Authorization: Bearer <token>"} content={"error": "Missing or invalid Authorization header. Use: Authorization: Bearer <token>"}
) )
token = auth_header.replace('Bearer ', '') token = auth_header.replace('Bearer ', '')
# First check global tokens (for backward compatibility)
allowed_tokens = server_config.get('auth_tokens', []) allowed_tokens = server_config.get('auth_tokens', [])
if token in allowed_tokens:
if token not in allowed_tokens: # Store global token info in request state
return JSONResponse( request.state.user_id = None
status_code=403, request.state.token_id = None
content={"error": "Invalid authentication token"} request.state.is_global_token = True
) else:
# Check user API tokens
from aisbf.database import get_database
db = get_database()
user_auth = db.authenticate_user_token(token)
if user_auth:
# Store user token info in request state
request.state.user_id = user_auth['user_id']
request.state.token_id = user_auth['token_id']
request.state.is_global_token = False
# Record token usage for analytics
# We'll do this asynchronously to avoid blocking the request
import asyncio
asyncio.create_task(record_token_usage_async(user_auth['user_id'], user_auth['token_id']))
else:
return JSONResponse(
status_code=403,
content={"error": "Invalid authentication token"}
)
else:
# Auth not enabled, set default state
request.state.user_id = None
request.state.token_id = None
request.state.is_global_token = False
response = await call_next(request) response = await call_next(request)
return response return response
async def record_token_usage_async(user_id: int, token_id: int):
"""Asynchronously record token usage"""
try:
from aisbf.database import get_database
db = get_database()
# Record with dummy values for now - will be updated when we know the actual usage
db.record_user_token_usage(user_id, token_id, '', '', 0)
except Exception as e:
logger.warning(f"Failed to record token usage: {e}")
# Exception handler for validation errors # Exception handler for validation errors
@app.exception_handler(RequestValidationError) @app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError): async def validation_exception_handler(request: Request, exc: RequestValidationError):
...@@ -1001,18 +1075,35 @@ async def dashboard_login_page(request: Request): ...@@ -1001,18 +1075,35 @@ async def dashboard_login_page(request: Request):
@app.post("/dashboard/login") @app.post("/dashboard/login")
async def dashboard_login(request: Request, username: str = Form(...), password: str = Form(...)): async def dashboard_login(request: Request, username: str = Form(...), password: str = Form(...)):
"""Handle dashboard login""" """Handle dashboard login"""
dashboard_config = server_config.get('dashboard_config', {}) if server_config else {} from aisbf.database import get_database
stored_username = dashboard_config.get('username', 'admin')
stored_password_hash = dashboard_config.get('password', '8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918')
# Hash the submitted password # Hash the submitted password
password_hash = hashlib.sha256(password.encode()).hexdigest() password_hash = hashlib.sha256(password.encode()).hexdigest()
# Compare username and hashed password # Try database authentication first
db = get_database()
user = db.authenticate_user(username, password_hash)
if user:
# Database user authenticated
request.session['logged_in'] = True
request.session['username'] = username
request.session['role'] = user['role']
request.session['user_id'] = user['id']
return RedirectResponse(url=url_for(request, "/dashboard"), status_code=303)
# Fallback to config admin
dashboard_config = server_config.get('dashboard_config', {}) if server_config else {}
stored_username = dashboard_config.get('username', 'admin')
stored_password_hash = dashboard_config.get('password', '8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918')
if username == stored_username and password_hash == stored_password_hash: if username == stored_username and password_hash == stored_password_hash:
request.session['logged_in'] = True request.session['logged_in'] = True
request.session['username'] = username request.session['username'] = username
request.session['role'] = 'admin'
request.session['user_id'] = None # Config admin has no user_id
return RedirectResponse(url=url_for(request, "/dashboard"), status_code=303) return RedirectResponse(url=url_for(request, "/dashboard"), status_code=303)
return templates.TemplateResponse("dashboard/login.html", {"request": request, "error": "Invalid credentials"}) return templates.TemplateResponse("dashboard/login.html", {"request": request, "error": "Invalid credentials"})
@app.get("/dashboard/logout") @app.get("/dashboard/logout")
...@@ -1027,6 +1118,15 @@ def require_dashboard_auth(request: Request): ...@@ -1027,6 +1118,15 @@ def require_dashboard_auth(request: Request):
return RedirectResponse(url=url_for(request, "/dashboard/login"), status_code=303) return RedirectResponse(url=url_for(request, "/dashboard/login"), status_code=303)
return None return None
def require_admin(request: Request):
"""Check if user is admin"""
auth_check = require_dashboard_auth(request)
if auth_check:
return auth_check
if request.session.get('role') != 'admin':
return RedirectResponse(url=url_for(request, "/dashboard"), status_code=303)
return None
@app.get("/dashboard", response_class=HTMLResponse) @app.get("/dashboard", response_class=HTMLResponse)
async def dashboard_index(request: Request): async def dashboard_index(request: Request):
"""Dashboard overview page""" """Dashboard overview page"""
...@@ -1046,14 +1146,50 @@ async def dashboard_index(request: Request): ...@@ -1046,14 +1146,50 @@ async def dashboard_index(request: Request):
}) })
else: else:
# User dashboard - show user stats # User dashboard - show user stats
return templates.TemplateResponse("dashboard/index.html", { from aisbf.database import get_database
db = get_database()
user_id = request.session.get('user_id')
# Get user statistics
usage_stats = {
'total_tokens': 0,
'requests_today': 0
}
if user_id:
# Get token usage for this user
token_usage = db.get_user_token_usage(user_id)
usage_stats['total_tokens'] = sum(row['token_count'] for row in token_usage)
# Count requests today
from datetime import datetime, timedelta
today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
usage_stats['requests_today'] = len([
row for row in token_usage
if datetime.fromisoformat(row['timestamp']) >= today
])
# Get user config counts
providers_count = len(db.get_user_providers(user_id))
rotations_count = len(db.get_user_rotations(user_id))
autoselects_count = len(db.get_user_autoselects(user_id))
# Get recent activity (last 10)
recent_activity = token_usage[-10:] if token_usage else []
else:
providers_count = 0
rotations_count = 0
autoselects_count = 0
recent_activity = []
return templates.TemplateResponse("dashboard/user_index.html", {
"request": request, "request": request,
"session": request.session, "session": request.session,
"user_message": "User dashboard - usage statistics and configuration management coming soon", "usage_stats": usage_stats,
"providers_count": 0, "providers_count": providers_count,
"rotations_count": 0, "rotations_count": rotations_count,
"autoselect_count": 0, "autoselects_count": autoselects_count,
"server_config": {} "recent_activity": recent_activity
}) })
@app.get("/dashboard/providers", response_class=HTMLResponse) @app.get("/dashboard/providers", response_class=HTMLResponse)
...@@ -1622,6 +1758,314 @@ async def dashboard_restart(request: Request): ...@@ -1622,6 +1758,314 @@ async def dashboard_restart(request: Request):
return JSONResponse({"message": "Server is restarting..."}) return JSONResponse({"message": "Server is restarting..."})
# User-specific configuration management routes
@app.get("/dashboard/user/providers", response_class=HTMLResponse)
async def dashboard_user_providers(request: Request):
"""User provider management page"""
auth_check = require_dashboard_auth(request)
if auth_check:
return auth_check
user_id = request.session.get('user_id')
if not user_id:
return RedirectResponse(url=url_for(request, "/dashboard/login"), status_code=303)
from aisbf.database import get_database
db = get_database()
# Get user-specific providers
user_providers = db.get_user_providers(user_id)
return templates.TemplateResponse("dashboard/user_providers.html", {
"request": request,
"session": request.session,
"user_providers_json": json.dumps(user_providers),
"user_id": user_id
})
@app.post("/dashboard/user/providers")
async def dashboard_user_providers_save(request: Request, provider_name: str = Form(...), provider_config: str = Form(...)):
"""Save user-specific provider configuration"""
auth_check = require_dashboard_auth(request)
if auth_check:
return auth_check
user_id = request.session.get('user_id')
if not user_id:
return RedirectResponse(url=url_for(request, "/dashboard/login"), status_code=303)
from aisbf.database import get_database
db = get_database()
try:
# Validate JSON
provider_data = json.loads(provider_config)
# Save to database
db.save_user_provider(user_id, provider_name, provider_data)
return RedirectResponse(url=url_for(request, "/dashboard/user/providers"), status_code=303)
except json.JSONDecodeError as e:
# Reload current providers on error
user_providers = db.get_user_providers(user_id)
return templates.TemplateResponse("dashboard/user_providers.html", {
"request": request,
"session": request.session,
"user_providers_json": json.dumps(user_providers),
"user_id": user_id,
"error": f"Invalid JSON: {str(e)}"
})
@app.delete("/dashboard/user/providers/{provider_name}")
async def dashboard_user_providers_delete(request: Request, provider_name: str):
"""Delete user-specific provider configuration"""
auth_check = require_dashboard_auth(request)
if auth_check:
return auth_check
user_id = request.session.get('user_id')
if not user_id:
return JSONResponse(status_code=401, content={"error": "Not authenticated"})
from aisbf.database import get_database
db = get_database()
try:
db.delete_user_provider(user_id, provider_name)
return JSONResponse({"message": "Provider deleted successfully"})
except Exception as e:
return JSONResponse(status_code=500, content={"error": str(e)})
# User-specific rotation management routes
@app.get("/dashboard/user/rotations", response_class=HTMLResponse)
async def dashboard_user_rotations(request: Request):
"""User rotation management page"""
auth_check = require_dashboard_auth(request)
if auth_check:
return auth_check
user_id = request.session.get('user_id')
if not user_id:
return RedirectResponse(url=url_for(request, "/dashboard/login"), status_code=303)
from aisbf.database import get_database
db = get_database()
# Get user-specific rotations
user_rotations = db.get_user_rotations(user_id)
return templates.TemplateResponse("dashboard/user_rotations.html", {
"request": request,
"session": request.session,
"user_rotations_json": json.dumps(user_rotations),
"user_id": user_id
})
@app.post("/dashboard/user/rotations")
async def dashboard_user_rotations_save(request: Request, rotation_name: str = Form(...), rotation_config: str = Form(...)):
"""Save user-specific rotation configuration"""
auth_check = require_dashboard_auth(request)
if auth_check:
return auth_check
user_id = request.session.get('user_id')
if not user_id:
return RedirectResponse(url=url_for(request, "/dashboard/login"), status_code=303)
from aisbf.database import get_database
db = get_database()
try:
# Validate JSON
rotation_data = json.loads(rotation_config)
# Save to database
db.save_user_rotation(user_id, rotation_name, rotation_data)
return RedirectResponse(url=url_for(request, "/dashboard/user/rotations"), status_code=303)
except json.JSONDecodeError as e:
# Reload current rotations on error
user_rotations = db.get_user_rotations(user_id)
return templates.TemplateResponse("dashboard/user_rotations.html", {
"request": request,
"session": request.session,
"user_rotations_json": json.dumps(user_rotations),
"user_id": user_id,
"error": f"Invalid JSON: {str(e)}"
})
@app.delete("/dashboard/user/rotations/{rotation_name}")
async def dashboard_user_rotations_delete(request: Request, rotation_name: str):
"""Delete user-specific rotation configuration"""
auth_check = require_dashboard_auth(request)
if auth_check:
return auth_check
user_id = request.session.get('user_id')
if not user_id:
return JSONResponse(status_code=401, content={"error": "Not authenticated"})
from aisbf.database import get_database
db = get_database()
try:
db.delete_user_rotation(user_id, rotation_name)
return JSONResponse({"message": "Rotation deleted successfully"})
except Exception as e:
return JSONResponse(status_code=500, content={"error": str(e)})
# User-specific autoselect management routes
@app.get("/dashboard/user/autoselects", response_class=HTMLResponse)
async def dashboard_user_autoselects(request: Request):
"""User autoselect management page"""
auth_check = require_dashboard_auth(request)
if auth_check:
return auth_check
user_id = request.session.get('user_id')
if not user_id:
return RedirectResponse(url=url_for(request, "/dashboard/login"), status_code=303)
from aisbf.database import get_database
db = get_database()
# Get user-specific autoselects
user_autoselects = db.get_user_autoselects(user_id)
return templates.TemplateResponse("dashboard/user_autoselects.html", {
"request": request,
"session": request.session,
"user_autoselects_json": json.dumps(user_autoselects),
"user_id": user_id
})
@app.post("/dashboard/user/autoselects")
async def dashboard_user_autoselects_save(request: Request, autoselect_name: str = Form(...), autoselect_config: str = Form(...)):
"""Save user-specific autoselect configuration"""
auth_check = require_dashboard_auth(request)
if auth_check:
return auth_check
user_id = request.session.get('user_id')
if not user_id:
return RedirectResponse(url=url_for(request, "/dashboard/login"), status_code=303)
from aisbf.database import get_database
db = get_database()
try:
# Validate JSON
autoselect_data = json.loads(autoselect_config)
# Save to database
db.save_user_autoselect(user_id, autoselect_name, autoselect_data)
return RedirectResponse(url=url_for(request, "/dashboard/user/autoselects"), status_code=303)
except json.JSONDecodeError as e:
# Reload current autoselects on error
user_autoselects = db.get_user_autoselects(user_id)
return templates.TemplateResponse("dashboard/user_autoselects.html", {
"request": request,
"session": request.session,
"user_autoselects_json": json.dumps(user_autoselects),
"user_id": user_id,
"error": f"Invalid JSON: {str(e)}"
})
@app.delete("/dashboard/user/autoselects/{autoselect_name}")
async def dashboard_user_autoselects_delete(request: Request, autoselect_name: str):
"""Delete user-specific autoselect configuration"""
auth_check = require_dashboard_auth(request)
if auth_check:
return auth_check
user_id = request.session.get('user_id')
if not user_id:
return JSONResponse(status_code=401, content={"error": "Not authenticated"})
from aisbf.database import get_database
db = get_database()
try:
db.delete_user_autoselect(user_id, autoselect_name)
return JSONResponse({"message": "Autoselect deleted successfully"})
except Exception as e:
return JSONResponse(status_code=500, content={"error": str(e)})
# User API token management routes
@app.get("/dashboard/user/tokens", response_class=HTMLResponse)
async def dashboard_user_tokens(request: Request):
"""User API token management page"""
auth_check = require_dashboard_auth(request)
if auth_check:
return auth_check
user_id = request.session.get('user_id')
if not user_id:
return RedirectResponse(url=url_for(request, "/dashboard/login"), status_code=303)
from aisbf.database import get_database
db = get_database()
# Get user API tokens
user_tokens = db.get_user_api_tokens(user_id)
return templates.TemplateResponse("dashboard/user_tokens.html", {
"request": request,
"session": request.session,
"user_tokens": user_tokens,
"user_id": user_id
})
@app.post("/dashboard/user/tokens")
async def dashboard_user_tokens_create(request: Request, description: str = Form("")):
"""Create a new user API token"""
auth_check = require_dashboard_auth(request)
if auth_check:
return auth_check
user_id = request.session.get('user_id')
if not user_id:
return JSONResponse(status_code=401, content={"error": "Not authenticated"})
from aisbf.database import get_database
import secrets
db = get_database()
# Generate a secure token
token = secrets.token_urlsafe(32)
try:
token_id = db.create_user_api_token(user_id, token, description.strip() or None)
return JSONResponse({
"message": "Token created successfully",
"token": token,
"token_id": token_id
})
except Exception as e:
return JSONResponse(status_code=500, content={"error": str(e)})
@app.delete("/dashboard/user/tokens/{token_id}")
async def dashboard_user_tokens_delete(request: Request, token_id: int):
"""Delete a user API token"""
auth_check = require_dashboard_auth(request)
if auth_check:
return auth_check
user_id = request.session.get('user_id')
if not user_id:
return JSONResponse(status_code=401, content={"error": "Not authenticated"})
from aisbf.database import get_database
db = get_database()
try:
db.delete_user_api_token(user_id, token_id)
return JSONResponse({"message": "Token deleted successfully"})
except Exception as e:
return JSONResponse(status_code=500, content={"error": str(e)})
@app.get("/dashboard/tor/status") @app.get("/dashboard/tor/status")
async def dashboard_tor_status(request: Request): async def dashboard_tor_status(request: Request):
"""Get TOR hidden service status""" """Get TOR hidden service status"""
...@@ -1867,10 +2311,14 @@ async def v1_chat_completions(request: Request, body: ChatCompletionRequest): ...@@ -1867,10 +2311,14 @@ async def v1_chat_completions(request: Request, body: ChatCompletionRequest):
detail=f"Autoselect '{actual_model}' not found. Available: {list(config.autoselect.keys())}" detail=f"Autoselect '{actual_model}' not found. Available: {list(config.autoselect.keys())}"
) )
body_dict['model'] = actual_model body_dict['model'] = actual_model
# Get user-specific handler
user_id = getattr(request.state, 'user_id', None)
handler = get_user_handler('autoselect', user_id)
if body.stream: if body.stream:
return await autoselect_handler.handle_autoselect_streaming_request(actual_model, body_dict) return await handler.handle_autoselect_streaming_request(actual_model, body_dict)
else: else:
return await autoselect_handler.handle_autoselect_request(actual_model, body_dict) return await handler.handle_autoselect_request(actual_model, body_dict)
# PATH 2: Check if it's a rotation (format: rotation/{name}) # PATH 2: Check if it's a rotation (format: rotation/{name})
if provider_id == "rotation": if provider_id == "rotation":
...@@ -1880,7 +2328,10 @@ async def v1_chat_completions(request: Request, body: ChatCompletionRequest): ...@@ -1880,7 +2328,10 @@ async def v1_chat_completions(request: Request, body: ChatCompletionRequest):
detail=f"Rotation '{actual_model}' not found. Available: {list(config.rotations.keys())}" detail=f"Rotation '{actual_model}' not found. Available: {list(config.rotations.keys())}"
) )
body_dict['model'] = actual_model body_dict['model'] = actual_model
return await rotation_handler.handle_rotation_request(actual_model, body_dict) # Get user-specific handler
user_id = getattr(request.state, 'user_id', None)
handler = get_user_handler('rotation', user_id)
return await handler.handle_rotation_request(actual_model, body_dict)
# PATH 1: Direct provider model (format: {provider}/{model}) # PATH 1: Direct provider model (format: {provider}/{model})
if provider_id not in config.providers: if provider_id not in config.providers:
...@@ -1899,10 +2350,14 @@ async def v1_chat_completions(request: Request, body: ChatCompletionRequest): ...@@ -1899,10 +2350,14 @@ async def v1_chat_completions(request: Request, body: ChatCompletionRequest):
# Handle as direct provider request # Handle as direct provider request
body_dict['model'] = actual_model body_dict['model'] = actual_model
# Get user-specific handler
user_id = getattr(request.state, 'user_id', None)
handler = get_user_handler('request', user_id)
if body.stream: if body.stream:
return await request_handler.handle_streaming_chat_completion(request, provider_id, body_dict) return await handler.handle_streaming_chat_completion(request, provider_id, body_dict)
else: else:
return await request_handler.handle_chat_completion(request, provider_id, body_dict) return await handler.handle_chat_completion(request, provider_id, body_dict)
@app.get("/api/models") @app.get("/api/models")
async def list_all_models(request: Request): async def list_all_models(request: Request):
...@@ -2069,6 +2524,10 @@ async def v1_audio_transcriptions(request: Request): ...@@ -2069,6 +2524,10 @@ async def v1_audio_transcriptions(request: Request):
detail=f"Provider '{provider_id}' credentials not available. Please configure credentials for this provider." detail=f"Provider '{provider_id}' credentials not available. Please configure credentials for this provider."
) )
# Get user-specific handler
user_id = getattr(request.state, 'user_id', None)
handler = get_user_handler('request', user_id)
# Create new form data with updated model # Create new form data with updated model
from starlette.datastructures import FormData from starlette.datastructures import FormData
updated_form = FormData() updated_form = FormData()
...@@ -2077,8 +2536,8 @@ async def v1_audio_transcriptions(request: Request): ...@@ -2077,8 +2536,8 @@ async def v1_audio_transcriptions(request: Request):
updated_form[key] = actual_model updated_form[key] = actual_model
else: else:
updated_form[key] = value updated_form[key] = value
return await request_handler.handle_audio_transcription(request, provider_id, updated_form) return await handler.handle_audio_transcription(request, provider_id, updated_form)
@app.post("/api/v1/audio/speech") @app.post("/api/v1/audio/speech")
async def v1_audio_speech(request: Request, body: dict): async def v1_audio_speech(request: Request, body: dict):
...@@ -2142,7 +2601,10 @@ async def v1_audio_speech(request: Request, body: dict): ...@@ -2142,7 +2601,10 @@ async def v1_audio_speech(request: Request, body: dict):
) )
body['model'] = actual_model body['model'] = actual_model
return await request_handler.handle_text_to_speech(request, provider_id, body) # Get user-specific handler
user_id = getattr(request.state, 'user_id', None)
handler = get_user_handler('request', user_id)
return await handler.handle_text_to_speech(request, provider_id, body)
@app.post("/api/v1/images/generations") @app.post("/api/v1/images/generations")
async def v1_image_generations(request: Request, body: dict): async def v1_image_generations(request: Request, body: dict):
...@@ -2206,7 +2668,10 @@ async def v1_image_generations(request: Request, body: dict): ...@@ -2206,7 +2668,10 @@ async def v1_image_generations(request: Request, body: dict):
) )
body['model'] = actual_model body['model'] = actual_model
return await request_handler.handle_image_generation(request, provider_id, body) # Get user-specific handler
user_id = getattr(request.state, 'user_id', None)
handler = get_user_handler('request', user_id)
return await handler.handle_image_generation(request, provider_id, body)
@app.post("/api/v1/embeddings") @app.post("/api/v1/embeddings")
async def v1_embeddings(request: Request, body: dict): async def v1_embeddings(request: Request, body: dict):
...@@ -2270,7 +2735,10 @@ async def v1_embeddings(request: Request, body: dict): ...@@ -2270,7 +2735,10 @@ async def v1_embeddings(request: Request, body: dict):
) )
body['model'] = actual_model body['model'] = actual_model
return await request_handler.handle_embeddings(request, provider_id, body) # Get user-specific handler
user_id = getattr(request.state, 'user_id', None)
handler = get_user_handler('request', user_id)
return await handler.handle_embeddings(request, provider_id, body)
@app.get("/api/rotations") @app.get("/api/rotations")
async def list_rotations(): async def list_rotations():
...@@ -2324,9 +2792,13 @@ async def rotation_chat_completions(request: Request, body: ChatCompletionReques ...@@ -2324,9 +2792,13 @@ async def rotation_chat_completions(request: Request, body: ChatCompletionReques
logger.debug("Handling rotation request") logger.debug("Handling rotation request")
try: try:
# Get user-specific handler
user_id = getattr(request.state, 'user_id', None)
handler = get_user_handler('rotation', user_id)
# The rotation handler handles streaming internally and returns # The rotation handler handles streaming internally and returns
# a StreamingResponse for streaming requests or a dict for non-streaming # a StreamingResponse for streaming requests or a dict for non-streaming
result = await rotation_handler.handle_rotation_request(body.model, body_dict) result = await handler.handle_rotation_request(body.model, body_dict)
logger.debug(f"Rotation response result type: {type(result)}") logger.debug(f"Rotation response result type: {type(result)}")
return result return result
except Exception as e: except Exception as e:
...@@ -2397,8 +2869,12 @@ async def autoselect_chat_completions(request: Request, body: ChatCompletionRequ ...@@ -2397,8 +2869,12 @@ async def autoselect_chat_completions(request: Request, body: ChatCompletionRequ
body_dict = body.model_dump() body_dict = body.model_dump()
# Get user-specific handler
user_id = getattr(request.state, 'user_id', None)
handler = get_user_handler('autoselect', user_id)
# Check if the model name corresponds to an autoselect configuration # Check if the model name corresponds to an autoselect configuration
if body.model not in config.autoselect: if body.model not in config.autoselect and (not user_id or body.model not in handler.user_autoselects):
logger.error(f"Model '{body.model}' not found in autoselect") logger.error(f"Model '{body.model}' not found in autoselect")
logger.error(f"Available autoselect: {list(config.autoselect.keys())}") logger.error(f"Available autoselect: {list(config.autoselect.keys())}")
raise HTTPException( raise HTTPException(
...@@ -2412,10 +2888,10 @@ async def autoselect_chat_completions(request: Request, body: ChatCompletionRequ ...@@ -2412,10 +2888,10 @@ async def autoselect_chat_completions(request: Request, body: ChatCompletionRequ
try: try:
if body.stream: if body.stream:
logger.debug("Handling streaming autoselect request") logger.debug("Handling streaming autoselect request")
return await autoselect_handler.handle_autoselect_streaming_request(body.model, body_dict) return await handler.handle_autoselect_streaming_request(body.model, body_dict)
else: else:
logger.debug("Handling non-streaming autoselect request") logger.debug("Handling non-streaming autoselect request")
result = await autoselect_handler.handle_autoselect_request(body.model, body_dict) result = await handler.handle_autoselect_request(body.model, body_dict)
logger.debug(f"Autoselect response result: {result}") logger.debug(f"Autoselect response result: {result}")
return result return result
except Exception as e: except Exception as e:
...@@ -2457,16 +2933,20 @@ async def chat_completions(provider_id: str, request: Request, body: ChatComplet ...@@ -2457,16 +2933,20 @@ async def chat_completions(provider_id: str, request: Request, body: ChatComplet
body_dict = body.model_dump() body_dict = body.model_dump()
# Get user-specific handler based on the type
user_id = getattr(request.state, 'user_id', None)
# Check if it's an autoselect # Check if it's an autoselect
if provider_id in config.autoselect: if provider_id in config.autoselect or (user_id and provider_id in get_user_handler('autoselect', user_id).user_autoselects):
logger.debug("Handling autoselect request") logger.debug("Handling autoselect request")
handler = get_user_handler('autoselect', user_id)
try: try:
if body.stream: if body.stream:
logger.debug("Handling streaming autoselect request") logger.debug("Handling streaming autoselect request")
return await autoselect_handler.handle_autoselect_streaming_request(provider_id, body_dict) return await handler.handle_autoselect_streaming_request(provider_id, body_dict)
else: else:
logger.debug("Handling non-streaming autoselect request") logger.debug("Handling non-streaming autoselect request")
result = await autoselect_handler.handle_autoselect_request(provider_id, body_dict) result = await handler.handle_autoselect_request(provider_id, body_dict)
logger.debug(f"Autoselect response result: {result}") logger.debug(f"Autoselect response result: {result}")
return result return result
except Exception as e: except Exception as e:
...@@ -2474,13 +2954,15 @@ async def chat_completions(provider_id: str, request: Request, body: ChatComplet ...@@ -2474,13 +2954,15 @@ async def chat_completions(provider_id: str, request: Request, body: ChatComplet
raise raise
# Check if it's a rotation # Check if it's a rotation
if provider_id in config.rotations: if provider_id in config.rotations or (user_id and provider_id in get_user_handler('rotation', user_id).user_rotations):
logger.info(f"Provider ID '{provider_id}' found in rotations") logger.info(f"Provider ID '{provider_id}' found in rotations")
logger.debug("Handling rotation request") logger.debug("Handling rotation request")
return await rotation_handler.handle_rotation_request(provider_id, body_dict) handler = get_user_handler('rotation', user_id)
return await handler.handle_rotation_request(provider_id, body_dict)
# Check if it's a provider # Check if it's a provider
if provider_id not in config.providers: handler = get_user_handler('request', user_id)
if provider_id not in config.providers and (not user_id or provider_id not in handler.user_providers):
logger.error(f"Provider ID '{provider_id}' not found in providers") logger.error(f"Provider ID '{provider_id}' not found in providers")
logger.error(f"Available providers: {list(config.providers.keys())}") logger.error(f"Available providers: {list(config.providers.keys())}")
logger.error(f"Available rotations: {list(config.rotations.keys())}") logger.error(f"Available rotations: {list(config.rotations.keys())}")
...@@ -2489,9 +2971,9 @@ async def chat_completions(provider_id: str, request: Request, body: ChatComplet ...@@ -2489,9 +2971,9 @@ async def chat_completions(provider_id: str, request: Request, body: ChatComplet
logger.info(f"Provider ID '{provider_id}' found in providers") logger.info(f"Provider ID '{provider_id}' found in providers")
provider_config = config.get_provider(provider_id) provider_config = handler.user_providers.get(provider_id) if user_id and provider_id in handler.user_providers else config.get_provider(provider_id)
logger.debug(f"Provider config: {provider_config}") logger.debug(f"Provider config: {provider_config}")
# Validate kiro credentials before processing request # Validate kiro credentials before processing request
if not validate_kiro_credentials(provider_id, provider_config): if not validate_kiro_credentials(provider_id, provider_config):
raise HTTPException( raise HTTPException(
...@@ -2502,10 +2984,10 @@ async def chat_completions(provider_id: str, request: Request, body: ChatComplet ...@@ -2502,10 +2984,10 @@ async def chat_completions(provider_id: str, request: Request, body: ChatComplet
try: try:
if body.stream: if body.stream:
logger.debug("Handling streaming chat completion") logger.debug("Handling streaming chat completion")
return await request_handler.handle_streaming_chat_completion(request, provider_id, body_dict) return await handler.handle_streaming_chat_completion(request, provider_id, body_dict)
else: else:
logger.debug("Handling non-streaming chat completion") logger.debug("Handling non-streaming chat completion")
result = await request_handler.handle_chat_completion(request, provider_id, body_dict) result = await handler.handle_chat_completion(request, provider_id, body_dict)
logger.debug(f"Response result: {result}") logger.debug(f"Response result: {result}")
return result return result
except Exception as e: except Exception as e:
...@@ -2516,11 +2998,15 @@ async def chat_completions(provider_id: str, request: Request, body: ChatComplet ...@@ -2516,11 +2998,15 @@ async def chat_completions(provider_id: str, request: Request, body: ChatComplet
async def list_models(request: Request, provider_id: str): async def list_models(request: Request, provider_id: str):
logger.debug(f"Received list_models request for provider: {provider_id}") logger.debug(f"Received list_models request for provider: {provider_id}")
# Get user-specific handler based on the type
user_id = getattr(request.state, 'user_id', None)
# Check if it's an autoselect # Check if it's an autoselect
if provider_id in config.autoselect: if provider_id in config.autoselect or (user_id and provider_id in get_user_handler('autoselect', user_id).user_autoselects):
logger.debug("Handling autoselect model list request") logger.debug("Handling autoselect model list request")
handler = get_user_handler('autoselect', user_id)
try: try:
result = await autoselect_handler.handle_autoselect_model_list(provider_id) result = await handler.handle_autoselect_model_list(provider_id)
logger.debug(f"Autoselect models result: {result}") logger.debug(f"Autoselect models result: {result}")
return result return result
except Exception as e: except Exception as e:
...@@ -2528,13 +3014,15 @@ async def list_models(request: Request, provider_id: str): ...@@ -2528,13 +3014,15 @@ async def list_models(request: Request, provider_id: str):
raise raise
# Check if it's a rotation # Check if it's a rotation
if provider_id in config.rotations: if provider_id in config.rotations or (user_id and provider_id in get_user_handler('rotation', user_id).user_rotations):
logger.info(f"Provider ID '{provider_id}' found in rotations") logger.info(f"Provider ID '{provider_id}' found in rotations")
logger.debug("Handling rotation model list request") logger.debug("Handling rotation model list request")
return await rotation_handler.handle_rotation_model_list(provider_id) handler = get_user_handler('rotation', user_id)
return await handler.handle_rotation_model_list(provider_id)
# Check if it's a provider # Check if it's a provider
if provider_id not in config.providers: handler = get_user_handler('request', user_id)
if provider_id not in config.providers and (not user_id or provider_id not in handler.user_providers):
logger.error(f"Provider ID '{provider_id}' not found in providers") logger.error(f"Provider ID '{provider_id}' not found in providers")
logger.error(f"Available providers: {list(config.providers.keys())}") logger.error(f"Available providers: {list(config.providers.keys())}")
logger.error(f"Available rotations: {list(config.rotations.keys())}") logger.error(f"Available rotations: {list(config.rotations.keys())}")
...@@ -2543,11 +3031,9 @@ async def list_models(request: Request, provider_id: str): ...@@ -2543,11 +3031,9 @@ async def list_models(request: Request, provider_id: str):
logger.info(f"Provider ID '{provider_id}' found in providers") logger.info(f"Provider ID '{provider_id}' found in providers")
provider_config = config.get_provider(provider_id)
try: try:
logger.debug("Handling model list request") logger.debug("Handling model list request")
result = await request_handler.handle_model_list(request, provider_id) result = await handler.handle_model_list(request, provider_id)
logger.debug(f"Models result: {result}") logger.debug(f"Models result: {result}")
return result return result
except Exception as e: except Exception as e:
...@@ -2615,6 +3101,10 @@ async def audio_transcriptions(request: Request): ...@@ -2615,6 +3101,10 @@ async def audio_transcriptions(request: Request):
detail=f"Provider '{provider_id}' not found. Available: {list(config.providers.keys())}" detail=f"Provider '{provider_id}' not found. Available: {list(config.providers.keys())}"
) )
# Get user-specific handler
user_id = getattr(request.state, 'user_id', None)
handler = get_user_handler('request', user_id)
# Create new form data with updated model # Create new form data with updated model
from starlette.datastructures import FormData from starlette.datastructures import FormData
updated_form = FormData() updated_form = FormData()
...@@ -2623,8 +3113,8 @@ async def audio_transcriptions(request: Request): ...@@ -2623,8 +3113,8 @@ async def audio_transcriptions(request: Request):
updated_form[key] = actual_model updated_form[key] = actual_model
else: else:
updated_form[key] = value updated_form[key] = value
return await request_handler.handle_audio_transcription(request, provider_id, updated_form) return await handler.handle_audio_transcription(request, provider_id, updated_form)
@app.post("/api/audio/speech") @app.post("/api/audio/speech")
async def audio_speech(request: Request, body: dict): async def audio_speech(request: Request, body: dict):
...@@ -2678,9 +3168,12 @@ async def audio_speech(request: Request, body: dict): ...@@ -2678,9 +3168,12 @@ async def audio_speech(request: Request, body: dict):
status_code=400, status_code=400,
detail=f"Provider '{provider_id}' not found. Available: {list(config.providers.keys())}" detail=f"Provider '{provider_id}' not found. Available: {list(config.providers.keys())}"
) )
body['model'] = actual_model body['model'] = actual_model
return await request_handler.handle_text_to_speech(request, provider_id, body) # Get user-specific handler
user_id = getattr(request.state, 'user_id', None)
handler = get_user_handler('request', user_id)
return await handler.handle_text_to_speech(request, provider_id, body)
# Image endpoints (supports all three proxy paths) # Image endpoints (supports all three proxy paths)
@app.post("/api/images/generations") @app.post("/api/images/generations")
...@@ -2735,9 +3228,12 @@ async def image_generations(request: Request, body: dict): ...@@ -2735,9 +3228,12 @@ async def image_generations(request: Request, body: dict):
status_code=400, status_code=400,
detail=f"Provider '{provider_id}' not found. Available: {list(config.providers.keys())}" detail=f"Provider '{provider_id}' not found. Available: {list(config.providers.keys())}"
) )
body['model'] = actual_model body['model'] = actual_model
return await request_handler.handle_image_generation(request, provider_id, body) # Get user-specific handler
user_id = getattr(request.state, 'user_id', None)
handler = get_user_handler('request', user_id)
return await handler.handle_image_generation(request, provider_id, body)
# Embeddings endpoint (supports all three proxy paths) # Embeddings endpoint (supports all three proxy paths)
@app.post("/api/embeddings") @app.post("/api/embeddings")
...@@ -2802,7 +3298,10 @@ async def embeddings(request: Request, body: dict): ...@@ -2802,7 +3298,10 @@ async def embeddings(request: Request, body: dict):
) )
body['model'] = actual_model body['model'] = actual_model
return await request_handler.handle_embeddings(request, provider_id, body) # Get user-specific handler
user_id = getattr(request.state, 'user_id', None)
handler = get_user_handler('request', user_id)
return await handler.handle_embeddings(request, provider_id, body)
# Content proxy endpoint # Content proxy endpoint
@app.get("/api/proxy/{content_id}") @app.get("/api/proxy/{content_id}")
...@@ -2812,6 +3311,7 @@ async def proxy_content(content_id: str): ...@@ -2812,6 +3311,7 @@ async def proxy_content(content_id: str):
logger.info(f"Content ID: {content_id}") logger.info(f"Content ID: {content_id}")
try: try:
# Get user-specific handler (use global for content proxy as it's shared)
result = await request_handler.handle_content_proxy(content_id) result = await request_handler.handle_content_proxy(content_id)
return result return result
except Exception as e: except Exception as e:
...@@ -3095,7 +3595,9 @@ async def mcp_sse(request: Request): ...@@ -3095,7 +3595,9 @@ async def mcp_sse(request: Request):
return return
try: try:
result = await mcp_server.handle_tool_call(tool_name, arguments, auth_level) # Get user_id from request state if available
user_id = getattr(request.state, 'user_id', None)
result = await mcp_server.handle_tool_call(tool_name, arguments, auth_level, user_id)
response = { response = {
"jsonrpc": "2.0", "jsonrpc": "2.0",
"id": request_id, "id": request_id,
...@@ -3218,7 +3720,9 @@ async def mcp_post(request: Request): ...@@ -3218,7 +3720,9 @@ async def mcp_post(request: Request):
) )
try: try:
result = await mcp_server.handle_tool_call(tool_name, arguments, auth_level) # Get user_id from request state if available
user_id = getattr(request.state, 'user_id', None)
result = await mcp_server.handle_tool_call(tool_name, arguments, auth_level, user_id)
return { return {
"jsonrpc": "2.0", "jsonrpc": "2.0",
"id": request_id, "id": request_id,
...@@ -3299,7 +3803,9 @@ async def mcp_call_tool(request: Request): ...@@ -3299,7 +3803,9 @@ async def mcp_call_tool(request: Request):
) )
try: try:
result = await mcp_server.handle_tool_call(tool_name, arguments, auth_level) # Get user_id from request state if available
user_id = getattr(request.state, 'user_id', None)
result = await mcp_server.handle_tool_call(tool_name, arguments, auth_level, user_id)
return {"result": result} return {"result": result}
except HTTPException as e: except HTTPException as e:
return JSONResponse( return JSONResponse(
......
{% extends "base.html" %}
{% block title %}My Autoselects - AISBF{% endblock %}
{% block content %}
<div class="container">
<h1>My Autoselects</h1>
<p>Manage your personal autoselect configurations</p>
{% if success %}
<div class="alert alert-success">{{ success }}</div>
{% endif %}
{% if error %}
<div class="alert alert-error">{{ error }}</div>
{% endif %}
<div class="card">
<h2>Autoselect Configurations</h2>
<div id="autoselects-list">
<!-- Autoselects will be loaded here -->
</div>
<button class="btn btn-primary" onclick="showAddAutoselectModal()">Add New Autoselect</button>
</div>
</div>
<!-- Modal for adding/editing autoselects -->
<div id="autoselect-modal" class="modal" style="display: none;">
<div class="modal-content">
<div class="modal-header">
<h3 id="modal-title">Add Autoselect</h3>
<button class="close-btn" onclick="closeModal()">&times;</button>
</div>
<div class="modal-body">
<form id="autoselect-form">
<div class="form-group">
<label for="autoselect-name">Autoselect Name:</label>
<input type="text" id="autoselect-name" required>
</div>
<div class="form-group">
<label for="autoselect-config">Autoselect Configuration (JSON):</label>
<textarea id="autoselect-config" rows="20" placeholder='{"model_name": "autoselect-name", "description": "My autoselect config", "fallback": "provider/model"}'></textarea>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Save</button>
<button type="button" class="btn btn-secondary" onclick="closeModal()">Cancel</button>
</div>
</form>
</div>
</div>
</div>
<script>
let autoselects = {{ user_autoselects_json | safe }};
let currentEditingIndex = -1;
function renderAutoselects() {
const container = document.getElementById('autoselects-list');
container.innerHTML = '';
if (autoselects.length === 0) {
container.innerHTML = '<p class="empty-state">No autoselects configured yet. Click "Add New Autoselect" to get started.</p>';
return;
}
autoselects.forEach((autoselect, index) => {
const div = document.createElement('div');
div.className = 'autoselect-item';
div.innerHTML = `
<div class="autoselect-header">
<h3>${autoselect.autoselect_id}</h3>
<div class="autoselect-actions">
<button class="btn btn-secondary btn-sm" onclick="editAutoselect(${index})">Edit</button>
<button class="btn btn-danger btn-sm" onclick="deleteAutoselect('${autoselect.autoselect_id}')">Delete</button>
</div>
</div>
<div class="autoselect-details">
<p><strong>Created:</strong> ${new Date(autoselect.created_at).toLocaleString()}</p>
<p><strong>Last Updated:</strong> ${new Date(autoselect.updated_at).toLocaleString()}</p>
<details>
<summary>Configuration (JSON)</summary>
<pre>${JSON.stringify(autoselect.config, null, 2)}</pre>
</details>
</div>
`;
container.appendChild(div);
});
}
function showAddAutoselectModal() {
currentEditingIndex = -1;
document.getElementById('modal-title').textContent = 'Add Autoselect';
document.getElementById('autoselect-name').value = '';
document.getElementById('autoselect-config').value = '{"model_name": "my-autoselect", "description": "My autoselect configuration", "fallback": "provider/model"}';
document.getElementById('autoselect-modal').style.display = 'block';
}
function editAutoselect(index) {
currentEditingIndex = index;
const autoselect = autoselects[index];
document.getElementById('modal-title').textContent = 'Edit Autoselect';
document.getElementById('autoselect-name').value = autoselect.autoselect_id;
document.getElementById('autoselect-name').readOnly = true;
document.getElementById('autoselect-config').value = JSON.stringify(autoselect.config, null, 2);
document.getElementById('autoselect-modal').style.display = 'block';
}
function closeModal() {
document.getElementById('autoselect-modal').style.display = 'none';
document.getElementById('autoselect-name').readOnly = false;
currentEditingIndex = -1;
}
function deleteAutoselect(autoselectName) {
if (!confirm(`Are you sure you want to delete the autoselect "${autoselectName}"? This action cannot be undone.`)) return;
fetch('{{ url_for(request, "/dashboard/user/autoselects") }}/' + encodeURIComponent(autoselectName), {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
}).then(response => {
if (response.ok) {
location.reload();
} else {
return response.json().then(data => {
throw new Error(data.error || 'Failed to delete autoselect');
});
}
}).catch(error => {
alert('Error: ' + error.message);
});
}
// Handle form submission
document.getElementById('autoselect-form').addEventListener('submit', function(e) {
e.preventDefault();
const autoselectName = document.getElementById('autoselect-name').value.trim();
const configText = document.getElementById('autoselect-config').value.trim();
if (!autoselectName) {
alert('Autoselect name is required');
return;
}
if (!configText) {
alert('Autoselect configuration is required');
return;
}
let configObj;
try {
configObj = JSON.parse(configText);
} catch (e) {
alert('Invalid JSON configuration: ' + e.message);
return;
}
const formData = new FormData();
formData.append('autoselect_name', autoselectName);
formData.append('autoselect_config', JSON.stringify(configObj));
const url = '{{ url_for(request, "/dashboard/user/autoselects") }}';
const method = currentEditingIndex >= 0 ? 'PUT' : 'POST';
fetch(url, {
method: method,
body: formData
}).then(response => {
if (response.ok) {
closeModal();
location.reload();
} else {
return response.text().then(text => {
throw new Error(text || 'Failed to save autoselect');
});
}
}).catch(error => {
alert('Error: ' + error.message);
});
});
renderAutoselects();
</script>
<style>
.autoselect-item {
background: #1a1a2e;
padding: 1rem;
margin: 1rem 0;
border-radius: 8px;
}
.autoselect-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.autoselect-header h3 {
margin: 0;
}
.autoselect-actions {
display: flex;
gap: 0.5rem;
}
.autoselect-details p {
margin: 0.5rem 0;
}
.autoselect-details pre {
background: #0f3460;
padding: 1rem;
border-radius: 4px;
overflow-x: auto;
white-space: pre-wrap;
word-wrap: break-word;
}
.empty-state {
text-align: center;
color: #a0a0a0;
padding: 2rem;
}
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background: #1a1a2e;
border-radius: 8px;
width: 90%;
max-width: 600px;
max-height: 80vh;
overflow-y: auto;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
border-bottom: 1px solid #0f3460;
}
.modal-header h3 {
margin: 0;
}
.close-btn {
background: none;
border: none;
color: #e0e0e0;
font-size: 1.5rem;
cursor: pointer;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
}
.modal-body {
padding: 1rem;
}
.form-group {
margin-bottom: 1rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: #e0e0e0;
}
.form-group input[type="text"],
.form-group textarea {
width: 100%;
padding: 0.5rem;
border: 1px solid #0f3460;
border-radius: 4px;
background: #16213e;
color: #e0e0e0;
font-family: monospace;
font-size: 14px;
}
.form-group textarea {
resize: vertical;
min-height: 300px;
}
.form-actions {
display: flex;
gap: 0.5rem;
justify-content: flex-end;
margin-top: 1rem;
}
</style>
{% endblock %}
\ No newline at end of file
{% extends "base.html" %}
{% block title %}User Dashboard - AISBF{% endblock %}
{% block content %}
<div class="container">
<h1>User Dashboard</h1>
<p>Welcome, {{ session.username }}!</p>
{% if success %}
<div class="alert alert-success">{{ success }}</div>
{% endif %}
{% if error %}
<div class="alert alert-error">{{ error }}</div>
{% endif %}
<!-- Usage Statistics -->
<div class="card">
<h2>Usage Statistics</h2>
<div class="stats-grid">
<div class="stat-item">
<h3>Total Tokens Used</h3>
<p class="stat-value">{{ usage_stats.total_tokens|default(0) }}</p>
</div>
<div class="stat-item">
<h3>Requests Today</h3>
<p class="stat-value">{{ usage_stats.requests_today|default(0) }}</p>
</div>
<div class="stat-item">
<h3>Active Providers</h3>
<p class="stat-value">{{ providers_count|default(0) }}</p>
</div>
<div class="stat-item">
<h3>Active Rotations</h3>
<p class="stat-value">{{ rotations_count|default(0) }}</p>
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="card">
<h2>Quick Actions</h2>
<div class="button-group">
<a href="{{ url_for(request, '/dashboard/user/providers') }}" class="btn btn-primary">Manage Providers</a>
<a href="{{ url_for(request, '/dashboard/user/rotations') }}" class="btn btn-primary">Manage Rotations</a>
<a href="{{ url_for(request, '/dashboard/user/autoselect') }}" class="btn btn-primary">Manage Autoselect</a>
<a href="{{ url_for(request, '/dashboard/user/tokens') }}" class="btn btn-primary">API Tokens</a>
</div>
</div>
<!-- Recent Activity -->
<div class="card">
<h2>Recent Activity</h2>
<table class="table">
<thead>
<tr>
<th>Timestamp</th>
<th>Provider</th>
<th>Model</th>
<th>Tokens</th>
</tr>
</thead>
<tbody>
{% if recent_activity %}
{% for activity in recent_activity %}
<tr>
<td>{{ activity.timestamp }}</td>
<td>{{ activity.provider_id }}</td>
<td>{{ activity.model }}</td>
<td>{{ activity.token_count }}</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="4" style="text-align: center;">No recent activity</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
<style>
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-top: 1rem;
}
.stat-item {
background: #1a1a2e;
padding: 1.5rem;
border-radius: 8px;
text-align: center;
}
.stat-item h3 {
font-size: 0.9rem;
color: #888;
margin-bottom: 0.5rem;
}
.stat-value {
font-size: 2rem;
font-weight: bold;
color: #16213e;
}
.button-group {
display: flex;
gap: 1rem;
flex-wrap: wrap;
margin-top: 1rem;
}
.table {
width: 100%;
margin-top: 1rem;
border-collapse: collapse;
}
.table th,
.table td {
padding: 0.75rem;
text-align: left;
border-bottom: 1px solid #333;
}
.table th {
background: #1a1a2e;
font-weight: bold;
}
.table tbody tr:hover {
background: #1a1a2e;
}
</style>
{% endblock %}
{% extends "base.html" %}
{% block title %}My Providers - AISBF{% endblock %}
{% block content %}
<div class="container">
<h1>My Providers</h1>
<p>Manage your personal provider configurations</p>
{% if success %}
<div class="alert alert-success">{{ success }}</div>
{% endif %}
{% if error %}
<div class="alert alert-error">{{ error }}</div>
{% endif %}
<div class="card">
<h2>Provider Configurations</h2>
<div id="providers-list">
<!-- Providers will be loaded here -->
</div>
<button class="btn btn-primary" onclick="showAddProviderModal()">Add New Provider</button>
</div>
</div>
<!-- Modal for adding/editing providers -->
<div id="provider-modal" class="modal" style="display: none;">
<div class="modal-content">
<div class="modal-header">
<h3 id="modal-title">Add Provider</h3>
<button class="close-btn" onclick="closeModal()">&times;</button>
</div>
<div class="modal-body">
<form id="provider-form">
<div class="form-group">
<label for="provider-name">Provider Name:</label>
<input type="text" id="provider-name" required>
</div>
<div class="form-group">
<label for="provider-config">Provider Configuration (JSON):</label>
<textarea id="provider-config" rows="20" placeholder='{"endpoint": "https://api.example.com", "api_key": "your-api-key", "models": []}'></textarea>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Save</button>
<button type="button" class="btn btn-secondary" onclick="closeModal()">Cancel</button>
</div>
</form>
</div>
</div>
</div>
<script>
let providers = {{ user_providers_json | safe }};
let currentEditingIndex = -1;
function renderProviders() {
const container = document.getElementById('providers-list');
container.innerHTML = '';
if (providers.length === 0) {
container.innerHTML = '<p class="empty-state">No providers configured yet. Click "Add New Provider" to get started.</p>';
return;
}
providers.forEach((provider, index) => {
const div = document.createElement('div');
div.className = 'provider-item';
div.innerHTML = `
<div class="provider-header">
<h3>${provider.provider_id}</h3>
<div class="provider-actions">
<button class="btn btn-secondary btn-sm" onclick="editProvider(${index})">Edit</button>
<button class="btn btn-danger btn-sm" onclick="deleteProvider('${provider.provider_id}')">Delete</button>
</div>
</div>
<div class="provider-details">
<p><strong>Created:</strong> ${new Date(provider.created_at).toLocaleString()}</p>
<p><strong>Last Updated:</strong> ${new Date(provider.updated_at).toLocaleString()}</p>
<details>
<summary>Configuration (JSON)</summary>
<pre>${JSON.stringify(provider.config, null, 2)}</pre>
</details>
</div>
`;
container.appendChild(div);
});
}
function showAddProviderModal() {
currentEditingIndex = -1;
document.getElementById('modal-title').textContent = 'Add Provider';
document.getElementById('provider-name').value = '';
document.getElementById('provider-config').value = '{"endpoint": "https://api.example.com", "api_key": "your-api-key", "models": []}';
document.getElementById('provider-modal').style.display = 'block';
}
function editProvider(index) {
currentEditingIndex = index;
const provider = providers[index];
document.getElementById('modal-title').textContent = 'Edit Provider';
document.getElementById('provider-name').value = provider.provider_id;
document.getElementById('provider-name').readOnly = true;
document.getElementById('provider-config').value = JSON.stringify(provider.config, null, 2);
document.getElementById('provider-modal').style.display = 'block';
}
function closeModal() {
document.getElementById('provider-modal').style.display = 'none';
document.getElementById('provider-name').readOnly = false;
currentEditingIndex = -1;
}
function deleteProvider(providerName) {
if (!confirm(`Are you sure you want to delete the provider "${providerName}"? This action cannot be undone.`)) return;
fetch('{{ url_for(request, "/dashboard/user/providers") }}/' + encodeURIComponent(providerName), {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
}).then(response => {
if (response.ok) {
location.reload();
} else {
return response.json().then(data => {
throw new Error(data.error || 'Failed to delete provider');
});
}
}).catch(error => {
alert('Error: ' + error.message);
});
}
// Handle form submission
document.getElementById('provider-form').addEventListener('submit', function(e) {
e.preventDefault();
const providerName = document.getElementById('provider-name').value.trim();
const configText = document.getElementById('provider-config').value.trim();
if (!providerName) {
alert('Provider name is required');
return;
}
if (!configText) {
alert('Provider configuration is required');
return;
}
let configObj;
try {
configObj = JSON.parse(configText);
} catch (e) {
alert('Invalid JSON configuration: ' + e.message);
return;
}
const formData = new FormData();
formData.append('provider_name', providerName);
formData.append('provider_config', JSON.stringify(configObj));
const url = '{{ url_for(request, "/dashboard/user/providers") }}';
const method = currentEditingIndex >= 0 ? 'PUT' : 'POST';
fetch(url, {
method: method,
body: formData
}).then(response => {
if (response.ok) {
closeModal();
location.reload();
} else {
return response.text().then(text => {
throw new Error(text || 'Failed to save provider');
});
}
}).catch(error => {
alert('Error: ' + error.message);
});
});
renderProviders();
</script>
<style>
.provider-item {
background: #1a1a2e;
padding: 1rem;
margin: 1rem 0;
border-radius: 8px;
}
.provider-item h3 {
margin-top: 0;
}
.provider-item code {
background: #0f3460;
padding: 0.2rem 0.5rem;
border-radius: 4px;
}
</style>
{% endblock %}
{% extends "base.html" %}
{% block title %}My Rotations - AISBF{% endblock %}
{% block content %}
<div class="container">
<h1>My Rotations</h1>
<p>Manage your personal rotation configurations</p>
{% if success %}
<div class="alert alert-success">{{ success }}</div>
{% endif %}
{% if error %}
<div class="alert alert-error">{{ error }}</div>
{% endif %}
<div class="card">
<h2>Rotation Configurations</h2>
<div id="rotations-list">
<!-- Rotations will be loaded here -->
</div>
<button class="btn btn-primary" onclick="showAddRotationModal()">Add New Rotation</button>
</div>
</div>
<!-- Modal for adding/editing rotations -->
<div id="rotation-modal" class="modal" style="display: none;">
<div class="modal-content">
<div class="modal-header">
<h3 id="modal-title">Add Rotation</h3>
<button class="close-btn" onclick="closeModal()">&times;</button>
</div>
<div class="modal-body">
<form id="rotation-form">
<div class="form-group">
<label for="rotation-name">Rotation Name:</label>
<input type="text" id="rotation-name" required>
</div>
<div class="form-group">
<label for="rotation-config">Rotation Configuration (JSON):</label>
<textarea id="rotation-config" rows="20" placeholder='{"model_name": "rotation-name", "providers": []}'></textarea>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Save</button>
<button type="button" class="btn btn-secondary" onclick="closeModal()">Cancel</button>
</div>
</form>
</div>
</div>
</div>
<script>
let rotations = {{ user_rotations_json | safe }};
let currentEditingIndex = -1;
function renderRotations() {
const container = document.getElementById('rotations-list');
container.innerHTML = '';
if (rotations.length === 0) {
container.innerHTML = '<p class="empty-state">No rotations configured yet. Click "Add New Rotation" to get started.</p>';
return;
}
rotations.forEach((rotation, index) => {
const div = document.createElement('div');
div.className = 'rotation-item';
div.innerHTML = `
<div class="rotation-header">
<h3>${rotation.rotation_id}</h3>
<div class="rotation-actions">
<button class="btn btn-secondary btn-sm" onclick="editRotation(${index})">Edit</button>
<button class="btn btn-danger btn-sm" onclick="deleteRotation('${rotation.rotation_id}')">Delete</button>
</div>
</div>
<div class="rotation-details">
<p><strong>Created:</strong> ${new Date(rotation.created_at).toLocaleString()}</p>
<p><strong>Last Updated:</strong> ${new Date(rotation.updated_at).toLocaleString()}</p>
<details>
<summary>Configuration (JSON)</summary>
<pre>${JSON.stringify(rotation.config, null, 2)}</pre>
</details>
</div>
`;
container.appendChild(div);
});
}
function showAddRotationModal() {
currentEditingIndex = -1;
document.getElementById('modal-title').textContent = 'Add Rotation';
document.getElementById('rotation-name').value = '';
document.getElementById('rotation-config').value = '{"model_name": "my-rotation", "providers": []}';
document.getElementById('rotation-modal').style.display = 'block';
}
function editRotation(index) {
currentEditingIndex = index;
const rotation = rotations[index];
document.getElementById('modal-title').textContent = 'Edit Rotation';
document.getElementById('rotation-name').value = rotation.rotation_id;
document.getElementById('rotation-name').readOnly = true;
document.getElementById('rotation-config').value = JSON.stringify(rotation.config, null, 2);
document.getElementById('rotation-modal').style.display = 'block';
}
function closeModal() {
document.getElementById('rotation-modal').style.display = 'none';
document.getElementById('rotation-name').readOnly = false;
currentEditingIndex = -1;
}
function deleteRotation(rotationName) {
if (!confirm(`Are you sure you want to delete the rotation "${rotationName}"? This action cannot be undone.`)) return;
fetch('{{ url_for(request, "/dashboard/user/rotations") }}/' + encodeURIComponent(rotationName), {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
}).then(response => {
if (response.ok) {
location.reload();
} else {
return response.json().then(data => {
throw new Error(data.error || 'Failed to delete rotation');
});
}
}).catch(error => {
alert('Error: ' + error.message);
});
}
// Handle form submission
document.getElementById('rotation-form').addEventListener('submit', function(e) {
e.preventDefault();
const rotationName = document.getElementById('rotation-name').value.trim();
const configText = document.getElementById('rotation-config').value.trim();
if (!rotationName) {
alert('Rotation name is required');
return;
}
if (!configText) {
alert('Rotation configuration is required');
return;
}
let configObj;
try {
configObj = JSON.parse(configText);
} catch (e) {
alert('Invalid JSON configuration: ' + e.message);
return;
}
const formData = new FormData();
formData.append('rotation_name', rotationName);
formData.append('rotation_config', JSON.stringify(configObj));
const url = '{{ url_for(request, "/dashboard/user/rotations") }}';
const method = currentEditingIndex >= 0 ? 'PUT' : 'POST';
fetch(url, {
method: method,
body: formData
}).then(response => {
if (response.ok) {
closeModal();
location.reload();
} else {
return response.text().then(text => {
throw new Error(text || 'Failed to save rotation');
});
}
}).catch(error => {
alert('Error: ' + error.message);
});
});
renderRotations();
</script>
<style>
.rotation-item {
background: #1a1a2e;
padding: 1rem;
margin: 1rem 0;
border-radius: 8px;
}
.rotation-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.rotation-header h3 {
margin: 0;
}
.rotation-actions {
display: flex;
gap: 0.5rem;
}
.rotation-details p {
margin: 0.5rem 0;
}
.rotation-details pre {
background: #0f3460;
padding: 1rem;
border-radius: 4px;
overflow-x: auto;
white-space: pre-wrap;
word-wrap: break-word;
}
.empty-state {
text-align: center;
color: #a0a0a0;
padding: 2rem;
}
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background: #1a1a2e;
border-radius: 8px;
width: 90%;
max-width: 600px;
max-height: 80vh;
overflow-y: auto;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
border-bottom: 1px solid #0f3460;
}
.modal-header h3 {
margin: 0;
}
.close-btn {
background: none;
border: none;
color: #e0e0e0;
font-size: 1.5rem;
cursor: pointer;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
}
.modal-body {
padding: 1rem;
}
.form-group {
margin-bottom: 1rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: #e0e0e0;
}
.form-group input[type="text"],
.form-group textarea {
width: 100%;
padding: 0.5rem;
border: 1px solid #0f3460;
border-radius: 4px;
background: #16213e;
color: #e0e0e0;
font-family: monospace;
font-size: 14px;
}
.form-group textarea {
resize: vertical;
min-height: 300px;
}
.form-actions {
display: flex;
gap: 0.5rem;
justify-content: flex-end;
margin-top: 1rem;
}
</style>
{% endblock %}
\ No newline at end of file
{% extends "base.html" %}
{% block title %}My API Tokens - AISBF{% endblock %}
{% block content %}
<div class="container">
<h1>My API Tokens</h1>
<p>Manage your personal API tokens for accessing the AISBF API</p>
{% if success %}
<div class="alert alert-success">{{ success }}</div>
{% endif %}
{% if error %}
<div class="alert alert-error">{{ error }}</div>
{% endif %}
<div class="card">
<h2>API Tokens</h2>
<div id="tokens-list">
<!-- Tokens will be loaded here -->
</div>
<button class="btn btn-primary" onclick="createToken()">Create New Token</button>
</div>
<!-- Modal for creating tokens -->
<div id="token-modal" class="modal" style="display: none;">
<div class="modal-content">
<div class="modal-header">
<h3>Create New API Token</h3>
<button class="close-btn" onclick="closeModal()">&times;</button>
</div>
<div class="modal-body">
<div id="token-creation-form">
<form id="create-token-form">
<div class="form-group">
<label for="token-description">Description (optional):</label>
<input type="text" id="token-description" placeholder="e.g., My application token">
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Create Token</button>
<button type="button" class="btn btn-secondary" onclick="closeModal()">Cancel</button>
</div>
</form>
</div>
<div id="token-result" style="display: none;">
<div class="alert alert-success">
<h4>Token Created Successfully!</h4>
<p><strong>Token:</strong> <code id="created-token"></code></p>
<p class="warning-text">⚠️ <strong>Important:</strong> Copy this token now. You won't be able to see it again!</p>
<div class="form-actions">
<button type="button" class="btn btn-primary" onclick="copyToken()">Copy Token</button>
<button type="button" class="btn btn-secondary" onclick="closeModal()">Close</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
let tokens = {{ user_tokens | tojson }};
function renderTokens() {
const container = document.getElementById('tokens-list');
container.innerHTML = '';
if (tokens.length === 0) {
container.innerHTML = '<p class="empty-state">No API tokens created yet. Click "Create New Token" to get started.</p>';
return;
}
tokens.forEach((token) => {
const div = document.createElement('div');
div.className = 'token-item';
div.innerHTML = `
<div class="token-header">
<div class="token-info">
<h4>${token.description || 'No description'}</h4>
<p class="token-meta">
Created: ${new Date(token.created_at).toLocaleString()}
${token.last_used ? `| Last used: ${new Date(token.last_used).toLocaleString()}` : ''}
</p>
</div>
<div class="token-actions">
<span class="token-status ${token.is_active ? 'active' : 'inactive'}">
${token.is_active ? 'Active' : 'Inactive'}
</span>
<button class="btn btn-danger btn-sm" onclick="deleteToken(${token.id})">Delete</button>
</div>
</div>
<div class="token-details">
<p><strong>Token ID:</strong> ${token.id}</p>
<p><strong>Token:</strong> ${token.token.substring(0, 20)}...</p>
</div>
`;
container.appendChild(div);
});
}
function createToken() {
document.getElementById('token-creation-form').style.display = 'block';
document.getElementById('token-result').style.display = 'none';
document.getElementById('create-token-form').reset();
document.getElementById('token-modal').style.display = 'block';
}
function closeModal() {
document.getElementById('token-modal').style.display = 'none';
}
function deleteToken(tokenId) {
if (!confirm('Are you sure you want to delete this API token? This action cannot be undone and will immediately revoke access.')) return;
fetch('{{ url_for(request, "/dashboard/user/tokens") }}/' + tokenId, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
}).then(response => {
if (response.ok) {
location.reload();
} else {
return response.json().then(data => {
throw new Error(data.error || 'Failed to delete token');
});
}
}).catch(error => {
alert('Error: ' + error.message);
});
}
function copyToken() {
const tokenElement = document.getElementById('created-token');
const token = tokenElement.textContent;
navigator.clipboard.writeText(token).then(() => {
alert('Token copied to clipboard!');
}).catch(() => {
// Fallback for older browsers
const textArea = document.createElement('textarea');
textArea.value = token;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
alert('Token copied to clipboard!');
});
}
// Handle form submission
document.getElementById('create-token-form').addEventListener('submit', function(e) {
e.preventDefault();
const description = document.getElementById('token-description').value.trim();
const formData = new FormData();
if (description) {
formData.append('description', description);
}
fetch('{{ url_for(request, "/dashboard/user/tokens") }}', {
method: 'POST',
body: formData
}).then(response => {
if (response.ok) {
return response.json();
} else {
return response.json().then(data => {
throw new Error(data.error || 'Failed to create token');
});
}
}).then(data => {
// Show the created token
document.getElementById('created-token').textContent = data.token;
document.getElementById('token-creation-form').style.display = 'none';
document.getElementById('token-result').style.display = 'block';
// Refresh the token list
tokens.push({
id: data.token_id,
token: data.token,
description: description || null,
created_at: new Date().toISOString(),
last_used: null,
is_active: true
});
renderTokens();
}).catch(error => {
alert('Error: ' + error.message);
});
});
renderTokens();
</script>
<style>
.token-item {
background: #1a1a2e;
padding: 1rem;
margin: 1rem 0;
border-radius: 8px;
border: 1px solid #0f3460;
}
.token-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 1rem;
}
.token-info h4 {
margin: 0 0 0.5rem 0;
color: #e0e0e0;
}
.token-meta {
color: #a0a0a0;
font-size: 0.9rem;
margin: 0;
}
.token-actions {
display: flex;
align-items: center;
gap: 0.5rem;
}
.token-status {
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.8rem;
font-weight: 500;
}
.token-status.active {
background: #28a745;
color: white;
}
.token-status.inactive {
background: #dc3545;
color: white;
}
.token-details {
background: #16213e;
padding: 1rem;
border-radius: 4px;
font-family: monospace;
font-size: 0.9rem;
}
.token-details p {
margin: 0.5rem 0;
word-break: break-all;
}
.empty-state {
text-align: center;
color: #a0a0a0;
padding: 2rem;
}
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background: #1a1a2e;
border-radius: 8px;
width: 90%;
max-width: 500px;
max-height: 80vh;
overflow-y: auto;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
border-bottom: 1px solid #0f3460;
}
.modal-header h3 {
margin: 0;
}
.close-btn {
background: none;
border: none;
color: #e0e0e0;
font-size: 1.5rem;
cursor: pointer;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
}
.modal-body {
padding: 1rem;
}
.form-group {
margin-bottom: 1rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: #e0e0e0;
}
.form-group input[type="text"] {
width: 100%;
padding: 0.5rem;
border: 1px solid #0f3460;
border-radius: 4px;
background: #16213e;
color: #e0e0e0;
font-size: 14px;
}
.form-actions {
display: flex;
gap: 0.5rem;
justify-content: flex-end;
margin-top: 1rem;
}
.warning-text {
color: #ffc107;
font-weight: 500;
}
#created-token {
background: #0f3460;
padding: 0.5rem;
border-radius: 4px;
font-family: monospace;
word-break: break-all;
}
</style>
{% endblock %}
\ No newline at end of file
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