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:
......
This diff is collapsed.
...@@ -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:
......
This diff is collapsed.
{% 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