Fix wallet manager database compatibility and users table schema migrations

parent 37d57c28
...@@ -8,6 +8,7 @@ include cli.py ...@@ -8,6 +8,7 @@ include cli.py
recursive-include config *.json recursive-include config *.json
recursive-include config *.md recursive-include config *.md
recursive-include aisbf *.py recursive-include aisbf *.py
recursive-include aisbf/payments/wallet *.py
recursive-include templates *.html recursive-include templates *.html
recursive-include templates *.css recursive-include templates *.css
recursive-include templates *.js recursive-include templates *.js
......
...@@ -153,14 +153,22 @@ check_package_upgrade() { ...@@ -153,14 +153,22 @@ check_package_upgrade() {
# Function to create venv if it doesn't exist # Function to create venv if it doesn't exist
ensure_venv() { ensure_venv() {
if [ "$DEBUG" = "true" ]; then
echo "=== DEBUG: ensure_venv called ==="
echo "VENV_DIR: $VENV_DIR"
echo "SHARE_DIR: $SHARE_DIR"
fi
if [ ! -d "$VENV_DIR" ]; then if [ ! -d "$VENV_DIR" ]; then
echo "Creating virtual environment at $VENV_DIR" echo "Creating virtual environment at $VENV_DIR"
# Create venv with --system-site-packages to access system-installed aisbf # Create venv with --system-site-packages to access system-installed aisbf
[ "$DEBUG" = "true" ] && echo "=== DEBUG: Creating venv ==="
python3 -m venv --system-site-packages "$VENV_DIR" python3 -m venv --system-site-packages "$VENV_DIR"
# Install requirements if requirements.txt exists # Install requirements if requirements.txt exists
if [ -f "$SHARE_DIR/requirements.txt" ]; then if [ -f "$SHARE_DIR/requirements.txt" ]; then
echo "Installing requirements from $SHARE_DIR/requirements.txt" echo "Installing requirements from $SHARE_DIR/requirements.txt"
[ "$DEBUG" = "true" ] && echo "=== DEBUG: Installing requirements ==="
if ! "$VENV_DIR/bin/pip" install -r "$SHARE_DIR/requirements.txt"; then if ! "$VENV_DIR/bin/pip" install -r "$SHARE_DIR/requirements.txt"; then
echo "" echo ""
echo "==========================================" echo "=========================================="
...@@ -184,18 +192,22 @@ ensure_venv() { ...@@ -184,18 +192,22 @@ ensure_venv() {
echo "" echo ""
exit 1 exit 1
fi fi
[ "$DEBUG" = "true" ] && echo "=== DEBUG: Force reinstalling uvicorn ==="
# Force reinstall uvicorn in venv to ensure it's available inside the virtual environment # Force reinstall uvicorn in venv to ensure it's available inside the virtual environment
"$VENV_DIR/bin/pip" install --force-reinstall uvicorn "$VENV_DIR/bin/pip" install --force-reinstall uvicorn
fi fi
# Save version for future upgrade detection # Save version for future upgrade detection
[ "$DEBUG" = "true" ] && echo "=== DEBUG: Saving version info ==="
python3 -c "import aisbf; print(aisbf.__version__)" > "$VENV_DIR/.aisbf_version" 2>/dev/null || echo "unknown" > "$VENV_DIR/.aisbf_version" python3 -c "import aisbf; print(aisbf.__version__)" > "$VENV_DIR/.aisbf_version" 2>/dev/null || echo "unknown" > "$VENV_DIR/.aisbf_version"
else else
[ "$DEBUG" = "true" ] && echo "=== DEBUG: Venv already exists, checking for upgrades ==="
# Check if package was upgraded via pip # Check if package was upgraded via pip
if check_package_upgrade; then if check_package_upgrade; then
echo "Package upgrade detected, updating venv dependencies..." echo "Package upgrade detected, updating venv dependencies..."
# Only update requirements, aisbf is accessed from system site-packages # Only update requirements, aisbf is accessed from system site-packages
if [ -f "$SHARE_DIR/requirements.txt" ]; then if [ -f "$SHARE_DIR/requirements.txt" ]; then
[ "$DEBUG" = "true" ] && echo "=== DEBUG: Updating requirements ==="
if ! "$VENV_DIR/bin/pip" install -r "$SHARE_DIR/requirements.txt"; then if ! "$VENV_DIR/bin/pip" install -r "$SHARE_DIR/requirements.txt"; then
echo "" echo ""
echo "==========================================" echo "=========================================="
...@@ -207,13 +219,17 @@ ensure_venv() { ...@@ -207,13 +219,17 @@ ensure_venv() {
echo "" echo ""
exit 1 exit 1
fi fi
[ "$DEBUG" = "true" ] && echo "=== DEBUG: Force reinstalling uvicorn ==="
# Force reinstall uvicorn in venv to ensure it's available inside the virtual environment # Force reinstall uvicorn in venv to ensure it's available inside the virtual environment
"$VENV_DIR/bin/pip" install --force-reinstall uvicorn "$VENV_DIR/bin/pip" install --force-reinstall uvicorn
fi fi
python3 -c "import aisbf; print(aisbf.__version__)" > "$VENV_DIR/.aisbf_version" 2>/dev/null || echo "unknown" > "$VENV_DIR/.aisbf_version" python3 -c "import aisbf; print(aisbf.__version__)" > "$VENV_DIR/.aisbf_version" 2>/dev/null || echo "unknown" > "$VENV_DIR/.aisbf_version"
echo "Virtual environment updated successfully" echo "Virtual environment updated successfully"
else
[ "$DEBUG" = "true" ] && echo "=== DEBUG: No package upgrade detected ==="
fi fi
fi fi
[ "$DEBUG" = "true" ] && echo "=== DEBUG: ensure_venv completed ==="
} }
# Function to update venv packages (only install missing ones, no forced upgrades) # Function to update venv packages (only install missing ones, no forced upgrades)
...@@ -255,29 +271,65 @@ update_venv() { ...@@ -255,29 +271,65 @@ update_venv() {
# Function to start the server # Function to start the server
start_server() { start_server() {
echo "=== DEBUG: Starting start_server function ==="
echo "SHARE_DIR: $SHARE_DIR"
echo "VENV_DIR: $VENV_DIR"
echo "LOG_DIR: $LOG_DIR"
# Ensure venv exists # Ensure venv exists
echo "=== DEBUG: Ensuring venv exists ==="
ensure_venv ensure_venv
echo "=== DEBUG: Venv check complete ==="
# Get host and port from config # Get host and port from config
echo "=== DEBUG: Getting host and port ==="
HOST=$(get_host) HOST=$(get_host)
PORT=$(get_port) PORT=$(get_port)
echo "=== DEBUG: Host=$HOST, Port=$PORT ==="
# Activate the virtual environment # Activate the virtual environment
echo "=== DEBUG: Activating virtual environment ==="
source $VENV_DIR/bin/activate source $VENV_DIR/bin/activate
echo "=== DEBUG: Virtual environment activated ==="
# Check Python path and imports
echo "=== DEBUG: Checking Python environment ==="
echo "Python executable: $(which python3)"
python3 -c "import sys; print('Python version:', sys.version); print('Python path:', sys.path[:3])"
echo "=== DEBUG: Testing basic imports ==="
python3 -c "import uvicorn; print('uvicorn imported successfully')" 2>&1 || echo "ERROR: Failed to import uvicorn"
python3 -c "import fastapi; print('fastapi imported successfully')" 2>&1 || echo "ERROR: Failed to import fastapi"
# Change to share directory where main.py is located # Change to share directory where main.py is located
echo "=== DEBUG: Changing to share directory ==="
cd $SHARE_DIR cd $SHARE_DIR
echo "=== DEBUG: Current directory: $(pwd) ==="
ls -la main.py 2>/dev/null || echo "WARNING: main.py not found in $SHARE_DIR"
echo "Starting AISBF on $HOST:$PORT..." echo "Starting AISBF on $HOST:$PORT..."
# Check if debug mode is enabled # Check if debug mode is enabled
if [ "$DEBUG" = "true" ]; then if [ "$DEBUG" = "true" ]; then
echo "Debug mode enabled - showing all debug messages" echo "Debug mode enabled - showing all debug messages"
export AISBF_DEBUG=true export AISBF_DEBUG=true
fi fi
# Test importing main module before starting uvicorn
echo "=== DEBUG: Testing main module import ==="
python3 -c "
try:
import main
print('main module imported successfully')
except Exception as e:
print(f'ERROR: Failed to import main module: {e}')
import traceback
traceback.print_exc()
exit(1)
" 2>&1
# Start the proxy server - runs in foreground # Start the proxy server - runs in foreground
# Use exec to replace the shell process so signals are properly handled # Use exec to replace the shell process so signals are properly handled
echo "=== DEBUG: Starting uvicorn ==="
if [ "$DEBUG" = "true" ]; then if [ "$DEBUG" = "true" ]; then
exec uvicorn main:app --host $HOST --port $PORT --log-level debug 2>&1 | tee -a "$LOG_DIR/aisbf_stdout.log" exec uvicorn main:app --host $HOST --port $PORT --log-level debug 2>&1 | tee -a "$LOG_DIR/aisbf_stdout.log"
else else
......
...@@ -117,6 +117,63 @@ class DatabaseManager: ...@@ -117,6 +117,63 @@ class DatabaseManager:
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
return await loop.run_in_executor(self.executor, func, *args) return await loop.run_in_executor(self.executor, func, *args)
async def execute(self, sql: str, params: dict = None):
"""Execute SQL query and return result with mappings (compatible with AsyncSession interface)"""
def _sync_execute():
with self._get_connection() as conn:
cursor = conn.cursor()
cursor.row_factory = sqlite3.Row
params = params or {}
# Safe parameter handling - use native database parameter binding
if self.db_type == 'sqlite':
# SQLite natively supports :named parameters directly
cursor.execute(sql, params)
else:
# For MySQL, safely convert named parameters to %s placeholders
param_names = []
def replace_param(match):
param_names.append(match.group(1))
return '%s'
import re
processed_sql = re.sub(r':(\w+)', replace_param, sql)
params_list = [params[name] for name in param_names]
cursor.execute(processed_sql, params_list)
if cursor.description:
rows = [dict(row) for row in cursor.fetchall()]
# Simulate SQLAlchemy Result object with mappings() method
class ResultWrapper:
def mappings(self):
class MappingsWrapper:
def first(self):
return rows[0] if rows else None
def all(self):
return rows
return MappingsWrapper()
return ResultWrapper()
else:
class EmptyResult:
def mappings(self):
return []
return EmptyResult()
return await self._run_in_executor(_sync_execute)
async def commit(self):
"""Commit transaction (compatibility method for WalletManager)"""
# Transactions are auto-committed per operation in this implementation
pass
def begin(self):
"""Async context manager for transactions (compatibility)"""
class TransactionContext:
async def __aenter__(self):
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
pass
return TransactionContext()
async def record_context_dimension_async( async def record_context_dimension_async(
...@@ -3493,24 +3550,24 @@ def DatabaseManager__initialize_database(self): ...@@ -3493,24 +3550,24 @@ def DatabaseManager__initialize_database(self):
# pass # pass
# #
# Create users table for multi-user management # Create users table for multi-user management
# cursor.execute(f''' cursor.execute(f'''
# CREATE TABLE IF NOT EXISTS users ( CREATE TABLE IF NOT EXISTS users (
# id INTEGER PRIMARY KEY {auto_increment}, id INTEGER PRIMARY KEY {auto_increment},
# username VARCHAR(255) UNIQUE NOT NULL, username VARCHAR(255) UNIQUE NOT NULL,
# email VARCHAR(255) UNIQUE, email VARCHAR(255) UNIQUE,
# display_name VARCHAR(255), display_name VARCHAR(255),
# password_hash VARCHAR(255) NOT NULL, password_hash VARCHAR(255) NOT NULL,
# role VARCHAR(50) DEFAULT 'user', role VARCHAR(50) DEFAULT 'user',
# created_by VARCHAR(255), created_by VARCHAR(255),
# created_at TIMESTAMP DEFAULT {timestamp_default}, created_at TIMESTAMP DEFAULT {timestamp_default},
# last_login TIMESTAMP NULL, last_login TIMESTAMP NULL,
# is_active {boolean_type} DEFAULT 1, is_active {boolean_type} DEFAULT 1,
# email_verified {boolean_type} DEFAULT 0, email_verified {boolean_type} DEFAULT 0,
# verification_token VARCHAR(255), verification_token VARCHAR(255),
# verification_token_expires TIMESTAMP NULL, verification_token_expires TIMESTAMP NULL,
# last_verification_email_sent TIMESTAMP NULL last_verification_email_sent TIMESTAMP NULL
# ) )
# ''') ''')
# #
# Migration: Add display_name column if it doesn't exist # Migration: Add display_name column if it doesn't exist
# try: # try:
...@@ -3938,11 +3995,11 @@ def DatabaseManager__initialize_database(self): ...@@ -3938,11 +3995,11 @@ def DatabaseManager__initialize_database(self):
# logger.info("⚠️ CACHE DATABASE: Only minimal cache tables created - NO USER TABLES") # logger.info("⚠️ CACHE DATABASE: Only minimal cache tables created - NO USER TABLES")
# Run configuration database migrations if this is a CONFIG database # Run configuration database migrations if this is a CONFIG database
if self.database_type == DatabaseRegistry.TYPE_CONFIG: if self.database_type == DatabaseRegistry.TYPE_CONFIG:
self._run_config_migrations(cursor, auto_increment, timestamp_default, boolean_type) self._run_config_migrations(cursor, auto_increment, timestamp_default, boolean_type)
conn.commit() conn.commit()
logger.info(f"Database tables initialized successfully for {self.database_type} database") logger.info(f"Database tables initialized successfully for {self.database_type} database")
def DatabaseManager__create_config_tables(self, cursor, auto_increment, timestamp_default, boolean_type): def DatabaseManager__create_config_tables(self, cursor, auto_increment, timestamp_default, boolean_type):
...@@ -4200,35 +4257,49 @@ def DatabaseManager__run_config_migrations(self, cursor, auto_increment, timesta ...@@ -4200,35 +4257,49 @@ def DatabaseManager__run_config_migrations(self, cursor, auto_increment, timesta
except Exception as e: except Exception as e:
logger.warning(f"Migration check for default free tier: {e}") logger.warning(f"Migration check for default free tier: {e}")
# Migration: Ensure users table exists first # Users table is created with full schema earlier in migrations
try:
if self.db_type == 'sqlite':
cursor.execute("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, email VARCHAR(255) UNIQUE NOT NULL, password_hash VARCHAR(255), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)")
else:
cursor.execute("CREATE TABLE IF NOT EXISTS users (id INT AUTO_INCREMENT PRIMARY KEY, email VARCHAR(255) UNIQUE NOT NULL, password_hash VARCHAR(255), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)")
except Exception as e:
logger.warning(f"Failed to create users table: {e}")
# Migration: Add tier_id column to users table # Migration: Add all missing columns to users table
try: try:
if self.db_type == 'sqlite': if self.db_type == 'sqlite':
cursor.execute("PRAGMA table_info(users)") cursor.execute("PRAGMA table_info(users)")
columns = [row[1] for row in cursor.fetchall()] columns = [row[1] for row in cursor.fetchall()]
if 'tier_id' not in columns:
cursor.execute('ALTER TABLE users ADD COLUMN tier_id INTEGER DEFAULT 1') required_columns = [
cursor.execute('ALTER TABLE users ADD COLUMN subscription_expires TIMESTAMP NULL') ('username', 'VARCHAR(255) UNIQUE', 'email'),
logger.info("✅ Migration: Added tier_id and subscription_expires columns to users") ('display_name', 'VARCHAR(255)', 'username'),
('role', 'VARCHAR(50) DEFAULT \'user\'', 'password_hash'),
('created_by', 'VARCHAR(255)', 'role'),
('last_login', 'TIMESTAMP NULL', 'created_at'),
('is_active', f'{boolean_type} DEFAULT 1', 'last_login'),
('email_verified', f'{boolean_type} DEFAULT 0', 'is_active'),
('verification_token', 'VARCHAR(255)', 'email_verified'),
('verification_token_expires', 'TIMESTAMP NULL', 'verification_token'),
('last_verification_email_sent', 'TIMESTAMP NULL', 'verification_token_expires'),
('tier_id', 'INTEGER DEFAULT 1', 'updated_at'),
('subscription_expires', 'TIMESTAMP NULL', 'tier_id'),
('stripe_customer_id', 'VARCHAR(100)', 'subscription_expires'),
('reset_password_token', 'VARCHAR(255)', 'stripe_customer_id'),
('reset_password_token_expires', 'TIMESTAMP NULL', 'reset_password_token')
]
for col_name, col_def, after_col in required_columns:
if col_name not in columns:
cursor.execute(f'ALTER TABLE users ADD COLUMN {col_name} {col_def}')
logger.info(f"✅ Migration: Added {col_name} column to users")
# Set username = email for existing users
if 'username' in required_columns and 'username' not in columns:
cursor.execute('UPDATE users SET username = email WHERE username IS NULL')
cursor.execute('UPDATE users SET is_active = 1 WHERE is_active IS NULL')
cursor.execute('UPDATE users SET role = \'user\' WHERE role IS NULL')
logger.info("✅ Migration: Populated username, is_active and role for existing users")
else: else:
cursor.execute(""" # MySQL migrations would go here
SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS pass
WHERE TABLE_NAME = 'users' AND COLUMN_NAME = 'tier_id'
""")
if not cursor.fetchone():
cursor.execute('ALTER TABLE users ADD COLUMN tier_id INTEGER DEFAULT 1')
cursor.execute('ALTER TABLE users ADD COLUMN subscription_expires TIMESTAMP NULL')
logger.info("✅ Migration: Added tier_id and subscription_expires columns to users")
except Exception as e: except Exception as e:
logger.warning(f"Migration check for users.tier_id: {e}") logger.warning(f"Migration check for users table columns: {e}")
# Migration: Create payment_methods, user_subscriptions, payment_transactions tables # Migration: Create payment_methods, user_subscriptions, payment_transactions tables
for table_name, create_sql in [ for table_name, create_sql in [
......
...@@ -155,8 +155,8 @@ ...@@ -155,8 +155,8 @@
} }
}, },
"billing": { "billing": {
"currency": "USD", "currency": "EUR",
"currency_symbol": "$", "currency_symbol": "",
"currency_decimals": 2, "currency_decimals": 2,
"payment_methods": { "payment_methods": {
"paypal": { "paypal": {
......
...@@ -680,16 +680,21 @@ def initialize_app(custom_config_dir=None): ...@@ -680,16 +680,21 @@ def initialize_app(custom_config_dir=None):
async def fetch_provider_models(provider_id: str, user_id: Optional[int] = None) -> list: async def fetch_provider_models(provider_id: str, user_id: Optional[int] = None) -> list:
"""Fetch models from provider API and cache them""" """Fetch models from provider API and cache them"""
global _model_cache, _model_cache_timestamps global _model_cache, _model_cache_timestamps
logger.debug(f"=== FETCH_PROVIDER_MODELS START: {provider_id} ===")
try: try:
logger.debug(f"Fetching models from provider: {provider_id} (user_id: {user_id})") logger.debug(f"Fetching models from provider: {provider_id} (user_id: {user_id})")
# Create request handler with correct user context # Create request handler with correct user context
logger.debug(f"Creating RequestHandler for provider '{provider_id}' with user_id: {user_id}")
request_handler = RequestHandler(user_id=user_id) request_handler = RequestHandler(user_id=user_id)
logger.debug(f"RequestHandler created successfully for provider '{provider_id}'")
# Create a dummy request object for the handler # Create a dummy request object for the handler
logger.debug(f"Creating dummy request object for provider '{provider_id}'")
from starlette.requests import Request from starlette.requests import Request
from starlette.datastructures import Headers from starlette.datastructures import Headers
scope = { scope = {
"type": "http", "type": "http",
"method": "GET", "method": "GET",
...@@ -698,19 +703,27 @@ async def fetch_provider_models(provider_id: str, user_id: Optional[int] = None) ...@@ -698,19 +703,27 @@ async def fetch_provider_models(provider_id: str, user_id: Optional[int] = None)
"path": f"/api/{provider_id}/models", "path": f"/api/{provider_id}/models",
} }
dummy_request = Request(scope) dummy_request = Request(scope)
logger.debug(f"Dummy request created for path: {scope['path']}")
# Fetch models from provider API (use user context if available) # Fetch models from provider API (use user context if available)
logger.debug(f"Calling handle_model_list for provider '{provider_id}'")
models = await request_handler.handle_model_list(dummy_request, provider_id) models = await request_handler.handle_model_list(dummy_request, provider_id)
logger.debug(f"handle_model_list returned {len(models) if models else 0} models for provider '{provider_id}'")
# Cache the results - separate cache for users vs global # Cache the results - separate cache for users vs global
cache_key = f"{provider_id}:{user_id}" if user_id else provider_id cache_key = f"{provider_id}:{user_id}" if user_id else provider_id
_model_cache[cache_key] = models _model_cache[cache_key] = models
_model_cache_timestamps[cache_key] = time.time() _model_cache_timestamps[cache_key] = time.time()
logger.info(f"Cached {len(models)} models from provider: {provider_id}") logger.info(f"Cached {len(models)} models from provider: {provider_id}")
logger.debug(f"=== FETCH_PROVIDER_MODELS SUCCESS: {provider_id} ===")
return models return models
except Exception as e: except Exception as e:
logger.warning(f"Failed to fetch models from provider {provider_id}: {e}") logger.error(f"=== FETCH_PROVIDER_MODELS FAILED: {provider_id} ===")
logger.error(f"Failed to fetch models from provider {provider_id}: {e}")
logger.debug(f"Error type: {type(e).__name__}")
logger.debug(f"Error details: {str(e)}", exc_info=True)
logger.debug(f"=== FETCH_PROVIDER_MODELS END (FAILED): {provider_id} ===")
return [] return []
async def refresh_model_cache(): async def refresh_model_cache():
...@@ -1081,72 +1094,116 @@ async def startup_event(): ...@@ -1081,72 +1094,116 @@ async def startup_event():
# Pre-fetch models at startup for providers without local model config # Pre-fetch models at startup for providers without local model config
# For Kilo providers, check API key, OAuth2 file credentials, and database-stored credentials # For Kilo providers, check API key, OAuth2 file credentials, and database-stored credentials
logger.info("=== STARTUP MODEL PRE-FETCHING ===")
logger.info("Pre-fetching models from providers with dynamic model lists...") logger.info("Pre-fetching models from providers with dynamic model lists...")
prefetch_count = 0 prefetch_count = 0
total_providers_checked = 0
for provider_id, provider_config in config.providers.items(): for provider_id, provider_config in config.providers.items():
total_providers_checked += 1
logger.debug(f"Checking provider '{provider_id}' for model pre-fetching...")
if not (hasattr(provider_config, 'models') and provider_config.models): if not (hasattr(provider_config, 'models') and provider_config.models):
logger.debug(f"Provider '{provider_id}' has no local model config, attempting pre-fetch")
# For Kilo providers, check if any authentication method is available # For Kilo providers, check if any authentication method is available
provider_type = getattr(provider_config, 'type', '') provider_type = getattr(provider_config, 'type', '')
if provider_type in ('kilo', 'kilocode'): if provider_type in ('kilo', 'kilocode'):
logger.debug(f"Kilo provider '{provider_id}' detected, validating authentication...")
has_valid_auth = False has_valid_auth = False
# Check 1: API key # Check 1: API key
logger.debug(f" Checking API key for provider '{provider_id}'...")
api_key = getattr(provider_config, 'api_key', None) api_key = getattr(provider_config, 'api_key', None)
if api_key and not api_key.startswith('YOUR_'): if api_key and not api_key.startswith('YOUR_'):
has_valid_auth = True has_valid_auth = True
logger.info(f"Kilo provider '{provider_id}' has API key configured, fetching models...") logger.info(f" ✓ Kilo provider '{provider_id}' has API key configured, will fetch models")
else:
logger.debug(f" ✗ API key not configured or placeholder for provider '{provider_id}'")
# Check 2: OAuth2 credentials file # Check 2: OAuth2 credentials file
if not has_valid_auth: if not has_valid_auth:
logger.debug(f" Checking OAuth2 credentials file for provider '{provider_id}'...")
try: try:
from aisbf.auth.kilo import KiloOAuth2 from aisbf.auth.kilo import KiloOAuth2
kilo_config = getattr(provider_config, 'kilo_config', None) kilo_config = getattr(provider_config, 'kilo_config', None)
credentials_file = None credentials_file = None
api_base = getattr(provider_config, 'endpoint', 'https://api.kilo.ai') api_base = getattr(provider_config, 'endpoint', 'https://api.kilo.ai')
if kilo_config and isinstance(kilo_config, dict): if kilo_config and isinstance(kilo_config, dict):
credentials_file = kilo_config.get('credentials_file') credentials_file = kilo_config.get('credentials_file')
logger.debug(f" Credentials file from config: {credentials_file}")
# Override api_base from kilo_config if present # Override api_base from kilo_config if present
if 'api_base' in kilo_config and kilo_config['api_base']: if 'api_base' in kilo_config and kilo_config['api_base']:
api_base = kilo_config['api_base'] api_base = kilo_config['api_base']
logger.debug(f" API base from config: {api_base}")
logger.debug(f" Attempting OAuth2 authentication check...")
oauth2 = KiloOAuth2(credentials_file=credentials_file, api_base=api_base) oauth2 = KiloOAuth2(credentials_file=credentials_file, api_base=api_base)
if oauth2.is_authenticated(): if oauth2.is_authenticated():
has_valid_auth = True has_valid_auth = True
logger.info(f"Kilo provider '{provider_id}' has valid OAuth2 credentials file, fetching models...") logger.info(f" ✓ Kilo provider '{provider_id}' has valid OAuth2 credentials file, will fetch models")
else:
logger.debug(f" ✗ OAuth2 not authenticated for provider '{provider_id}'")
except Exception as e: except Exception as e:
logger.debug(f"Kilo provider '{provider_id}' OAuth2 file check failed: {e}") logger.warning(f" ✗ Kilo provider '{provider_id}' OAuth2 file check failed: {e}")
logger.debug(f" OAuth2 check error details: {type(e).__name__}: {str(e)}", exc_info=True)
# Check 3: Database-stored credentials (uploaded via dashboard) # Check 3: Database-stored credentials (uploaded via dashboard)
if not has_valid_auth: if not has_valid_auth:
logger.debug(f" Checking database credentials for provider '{provider_id}'...")
try: try:
db = DatabaseRegistry.get_config_database() db = DatabaseRegistry.get_config_database()
if db: if db:
logger.debug(f" Database available, checking for uploaded credentials...")
# Check for uploaded credentials files for this provider # Check for uploaded credentials files for this provider
auth_files = db.get_user_auth_files(0, provider_id) # 0 for admin/global auth_files = db.get_user_auth_files(0, provider_id) # 0 for admin/global
if auth_files: if auth_files:
logger.debug(f" Found {len(auth_files)} auth files for provider '{provider_id}'")
for auth_file in auth_files: for auth_file in auth_files:
file_type = auth_file.get('file_type', '') file_type = auth_file.get('file_type', '')
file_path = auth_file.get('file_path', '') file_path = auth_file.get('file_path', '')
logger.debug(f" Checking auth file: type={file_type}, path={file_path}")
if file_type in ('credentials', 'kilo_credentials', 'config') and file_path: if file_type in ('credentials', 'kilo_credentials', 'config') and file_path:
if os.path.exists(file_path): if os.path.exists(file_path):
has_valid_auth = True has_valid_auth = True
logger.info(f"Kilo provider '{provider_id}' has uploaded credentials in database, fetching models...") logger.info(f" ✓ Kilo provider '{provider_id}' has uploaded credentials in database, will fetch models")
logger.debug(f" Using credentials file: {file_path}")
break break
else:
logger.debug(f" Credentials file does not exist: {file_path}")
else:
logger.debug(f" No auth files found in database for provider '{provider_id}'")
else:
logger.debug(f" Database not available for credentials check")
except Exception as e: except Exception as e:
logger.debug(f"Kilo provider '{provider_id}' database credentials check failed: {e}") logger.warning(f" ✗ Kilo provider '{provider_id}' database credentials check failed: {e}")
logger.debug(f" Database credentials check error details: {type(e).__name__}: {str(e)}", exc_info=True)
if not has_valid_auth: if not has_valid_auth:
logger.info(f"Skipping model prefetch for Kilo provider '{provider_id}' (no API key, OAuth2 file, or uploaded credentials)") logger.info(f"Skipping model prefetch for Kilo provider '{provider_id}' (no valid authentication method found)")
logger.debug(f" Authentication methods checked: API key, OAuth2 file, database credentials")
continue continue
else:
logger.debug(f"Provider '{provider_id}' is not Kilo type (type: {provider_type}), proceeding with fetch")
logger.info(f"Attempting to pre-fetch models from provider '{provider_id}'...")
try: try:
models = await fetch_provider_models(provider_id) models = await fetch_provider_models(provider_id)
if models: if models:
prefetch_count += 1 prefetch_count += 1
logger.info(f"Pre-fetched {len(models)} models from provider: {provider_id}") logger.info(f"✓ Successfully pre-fetched {len(models)} models from provider: {provider_id}")
logger.debug(f" Models: {[m.get('id', m.get('name', 'unknown')) for m in models[:5]]}" + (f" ... and {len(models)-5} more" if len(models) > 5 else ""))
else:
logger.warning(f"✗ Pre-fetch returned empty model list from provider '{provider_id}'")
except Exception as e: except Exception as e:
logger.warning(f"Failed to pre-fetch models from provider {provider_id}: {e}") logger.error(f"✗ Failed to pre-fetch models from provider '{provider_id}': {e}")
logger.debug(f" Pre-fetch error details for '{provider_id}': {type(e).__name__}: {str(e)}", exc_info=True)
else:
logger.debug(f"Provider '{provider_id}' has local model config ({len(provider_config.models)} models), skipping pre-fetch")
logger.info(f"=== MODEL PRE-FETCHING COMPLETE ===")
logger.info(f"Checked {total_providers_checked} providers, successfully pre-fetched from {prefetch_count} providers")
if prefetch_count > 0: if prefetch_count > 0:
logger.info(f"Pre-fetched models from {prefetch_count} provider(s) at startup") logger.info(f"Pre-fetched models from {prefetch_count} provider(s) at startup")
...@@ -1172,8 +1229,9 @@ async def startup_event(): ...@@ -1172,8 +1229,9 @@ async def startup_event():
logger.info(f" Type: {provider_config.type}") logger.info(f" Type: {provider_config.type}")
logger.info(f" Endpoint: {provider_config.endpoint}") logger.info(f" Endpoint: {provider_config.endpoint}")
logger.info(f" API Key Required: {provider_config.api_key_required}") logger.info(f" API Key Required: {provider_config.api_key_required}")
# Check if API key is configured # Check authentication configuration
provider_type = getattr(provider_config, 'type', '')
if provider_config.api_key_required: if provider_config.api_key_required:
if provider_config.api_key: if provider_config.api_key:
logger.info(f" API Key: Configured ✓") logger.info(f" API Key: Configured ✓")
...@@ -1182,10 +1240,68 @@ async def startup_event(): ...@@ -1182,10 +1240,68 @@ async def startup_event():
logger.warning(f" API Key: NOT CONFIGURED ✗") logger.warning(f" API Key: NOT CONFIGURED ✗")
logger.warning(f" Status: WILL BE SKIPPED - API key required but not provided") logger.warning(f" Status: WILL BE SKIPPED - API key required but not provided")
logger.warning(f" Action: Add api_key to provider configuration in providers.json") logger.warning(f" Action: Add api_key to provider configuration in providers.json")
elif provider_type in ('kilo', 'kilocode'):
# Special handling for Kilo providers with multiple auth methods
logger.info(f" Authentication: Multiple methods supported")
auth_methods = []
# Check API key
api_key = getattr(provider_config, 'api_key', None)
if api_key and not api_key.startswith('YOUR_'):
auth_methods.append("API Key ✓")
else:
auth_methods.append("API Key ✗")
# Check OAuth2 file
try:
from aisbf.auth.kilo import KiloOAuth2
kilo_config = getattr(provider_config, 'kilo_config', None)
credentials_file = None
if kilo_config and isinstance(kilo_config, dict):
credentials_file = kilo_config.get('credentials_file')
oauth2 = KiloOAuth2(credentials_file=credentials_file)
if oauth2.is_authenticated():
auth_methods.append("OAuth2 File ✓")
else:
auth_methods.append("OAuth2 File ✗")
except:
auth_methods.append("OAuth2 File ✗ (check failed)")
# Check database credentials
try:
db = DatabaseRegistry.get_config_database()
if db:
auth_files = db.get_user_auth_files(0, provider_id)
if auth_files:
has_valid_file = False
for auth_file in auth_files:
file_type = auth_file.get('file_type', '')
file_path = auth_file.get('file_path', '')
if file_type in ('credentials', 'kilo_credentials', 'config') and file_path and os.path.exists(file_path):
has_valid_file = True
break
if has_valid_file:
auth_methods.append("Database Credentials ✓")
else:
auth_methods.append("Database Credentials ✗ (files exist but invalid)")
else:
auth_methods.append("Database Credentials ✗ (no files)")
else:
auth_methods.append("Database Credentials ✗ (no database)")
except:
auth_methods.append("Database Credentials ✗ (check failed)")
logger.info(f" Auth Methods: {', '.join(auth_methods)}")
if any("✓" in method for method in auth_methods):
logger.info(f" Status: Ready to use (at least one auth method available)")
else:
logger.warning(f" Status: WILL BE SKIPPED - No valid authentication methods found")
logger.warning(f" Action: Configure API key, OAuth2 credentials file, or upload credentials via dashboard")
else: else:
logger.info(f" API Key: Not required") logger.info(f" API Key: Not required")
logger.info(f" Status: Ready to use") logger.info(f" Status: Ready to use")
# Show model count if available # Show model count if available
if provider_config.models: if provider_config.models:
logger.info(f" Models Configured: {len(provider_config.models)}") logger.info(f" Models Configured: {len(provider_config.models)}")
...@@ -1194,7 +1310,7 @@ async def startup_event(): ...@@ -1194,7 +1310,7 @@ async def startup_event():
if len(provider_config.models) > 3: if len(provider_config.models) > 3:
logger.info(f" ... and {len(provider_config.models) - 3} more") logger.info(f" ... and {len(provider_config.models) - 3} more")
else: else:
logger.info(f" Models Configured: None (will use provider's default models)") logger.info(f" Models Configured: None (will use provider's default models or dynamic fetching)")
logger.info("") logger.info("")
logger.info("=" * 80) logger.info("=" * 80)
...@@ -1223,7 +1339,7 @@ async def startup_event(): ...@@ -1223,7 +1339,7 @@ async def startup_event():
'encryption_key': encryption_key, 'encryption_key': encryption_key,
'encryption_key_source': encryption_key_source, 'encryption_key_source': encryption_key_source,
'base_url': os.getenv('BASE_URL', 'http://localhost:17765'), 'base_url': os.getenv('BASE_URL', 'http://localhost:17765'),
'currency_code': 'USD', 'currency_code': 'EUR',
'btc_confirmations': 3, 'btc_confirmations': 3,
'eth_confirmations': 12 'eth_confirmations': 12
} }
...@@ -1953,8 +2069,11 @@ async def delete_payment_method(payment_method_id: int, request: Request): ...@@ -1953,8 +2069,11 @@ async def delete_payment_method(payment_method_id: int, request: Request):
return result return result
# Wallet API routes # Wallet API routes
from aisbf.payments.wallet.routes import router as wallet_router try:
app.include_router(wallet_router) from aisbf.payments.wallet.routes import router as wallet_router
app.include_router(wallet_router)
except ImportError:
logger.warning("Wallet routes not available - wallet functionality disabled")
@app.post("/api/webhooks/stripe") @app.post("/api/webhooks/stripe")
...@@ -7190,16 +7309,27 @@ async def api_get_crypto_prices(request: Request): ...@@ -7190,16 +7309,27 @@ async def api_get_crypto_prices(request: Request):
# Get currency settings # Get currency settings
currency_settings = db.get_currency_settings() currency_settings = db.get_currency_settings()
currency_code = currency_settings.get('currency_code', 'USD') currency_code = currency_settings.get('currency_code', 'EUR')
result = {} result = {}
# Cache for supported pairs
supported_pairs_cache = getattr(asyncio, '__pair_cache', {})
cache_expiry = getattr(asyncio, '__pair_cache_expiry', 0)
if time.time() > cache_expiry:
supported_pairs_cache = {}
cache_expiry = time.time() + 86400 # 24 hour cache
setattr(asyncio, '__pair_cache', supported_pairs_cache)
setattr(asyncio, '__pair_cache_expiry', cache_expiry)
# Fetch prices for each cryptocurrency # Fetch prices for each cryptocurrency
for crypto_symbol, crypto_name in [('BTC', 'btc'), ('ETH', 'eth'), ('USDT', 'usdt'), ('USDC', 'usdc')]: for crypto_symbol, crypto_name in [('BTC', 'btc'), ('ETH', 'eth'), ('USDT', 'usdt'), ('USDC', 'usdc')]:
prices = {} prices = {}
enabled_prices = [] enabled_prices = []
cache_key = f"{crypto_symbol}:{currency_code}"
# Fetch from Coinbase # Coinbase
if sources.get('coinbase', False): if sources.get('coinbase', False):
try: try:
async with httpx.AsyncClient(timeout=10.0) as client: async with httpx.AsyncClient(timeout=10.0) as client:
...@@ -7209,63 +7339,85 @@ async def api_get_crypto_prices(request: Request): ...@@ -7209,63 +7339,85 @@ async def api_get_crypto_prices(request: Request):
price = float(data['data']['amount']) price = float(data['data']['amount'])
prices['coinbase'] = price prices['coinbase'] = price
enabled_prices.append(price) enabled_prices.append(price)
supported_pairs_cache[f"coinbase:{cache_key}"] = True
except Exception as e: except Exception as e:
logger.warning(f"Error fetching Coinbase {crypto_symbol} price: {e}") supported_pairs_cache[f"coinbase:{cache_key}"] = False
logger.debug(f"Coinbase does not support {crypto_symbol}/{currency_code} pair: {e}")
prices['coinbase'] = None prices['coinbase'] = None
else: else:
prices['coinbase'] = None prices['coinbase'] = None
# Fetch from Binance # Binance
if sources.get('binance', False): if sources.get('binance', False):
try: try:
async with httpx.AsyncClient(timeout=10.0) as client: async with httpx.AsyncClient(timeout=10.0) as client:
# Binance uses USDT pairs for most, but handle stablecoins differently # Try direct pair first
if crypto_symbol in ['USDT', 'USDC']: symbol = f"{crypto_symbol}{currency_code}"
symbol = f'{crypto_symbol}USDT' if crypto_symbol == 'USDC' else 'USDCUSDT'
else:
symbol = f'{crypto_symbol}USDT' if currency_code == 'USD' else f'{crypto_symbol}{currency_code}'
response = await client.get(f'https://api.binance.com/api/v3/ticker/price?symbol={symbol}') response = await client.get(f'https://api.binance.com/api/v3/ticker/price?symbol={symbol}')
if response.status_code == 200:
if response.status_code != 200:
# Fallback to USDT pair if direct pair not available
symbol = f"{crypto_symbol}USDT"
response = await client.get(f'https://api.binance.com/api/v3/ticker/price?symbol={symbol}')
if response.status_code == 200:
# Get USD/EUR rate if needed
if currency_code != 'USD':
usd_resp = await client.get('https://api.coinbase.com/v2/prices/USD-EUR/spot')
if usd_resp.status_code == 200:
usd_eur = float(usd_resp.json()['data']['amount'])
data = response.json()
price = float(data['price']) * usd_eur
prices['binance'] = price
enabled_prices.append(price)
supported_pairs_cache[f"binance:{cache_key}"] = "usdt_fallback"
else:
data = response.json() data = response.json()
price = float(data['price']) price = float(data['price'])
prices['binance'] = price prices['binance'] = price
enabled_prices.append(price) enabled_prices.append(price)
supported_pairs_cache[f"binance:{cache_key}"] = True
except Exception as e: except Exception as e:
logger.warning(f"Error fetching Binance {crypto_symbol} price: {e}") supported_pairs_cache[f"binance:{cache_key}"] = False
logger.debug(f"Binance does not support {crypto_symbol}/{currency_code} pair: {e}")
prices['binance'] = None prices['binance'] = None
else: else:
prices['binance'] = None prices['binance'] = None
# Fetch from Kraken # Kraken
if sources.get('kraken', False): if sources.get('kraken', False):
try: try:
async with httpx.AsyncClient(timeout=10.0) as client: async with httpx.AsyncClient(timeout=10.0) as client:
# Kraken uses different symbols # Kraken symbols
if crypto_symbol == 'BTC': kraken_prefix = {
pair = f'XXBTZ{currency_code}' 'BTC': 'XXBT',
elif crypto_symbol == 'ETH': 'ETH': 'XETH',
pair = f'XETHZ{currency_code}' 'USDT': 'USDT',
elif crypto_symbol == 'USDT': 'USDC': 'USDC'
pair = f'USDTZ{currency_code}' }.get(crypto_symbol, crypto_symbol)
elif crypto_symbol == 'USDC':
pair = f'USDCZ{currency_code}'
pair = f"{kraken_prefix}Z{currency_code}"
response = await client.get(f'https://api.kraken.com/0/public/Ticker?pair={pair}') response = await client.get(f'https://api.kraken.com/0/public/Ticker?pair={pair}')
if response.status_code == 200: if response.status_code == 200:
data = response.json() data = response.json()
if 'result' in data and data['result']: if not data.get('error') and 'result' in data and data['result']:
# Get first result key
result_key = list(data['result'].keys())[0] result_key = list(data['result'].keys())[0]
price = float(data['result'][result_key]['c'][0]) price = float(data['result'][result_key]['c'][0])
prices['kraken'] = price prices['kraken'] = price
enabled_prices.append(price) enabled_prices.append(price)
supported_pairs_cache[f"kraken:{cache_key}"] = True
else:
supported_pairs_cache[f"kraken:{cache_key}"] = False
except Exception as e: except Exception as e:
logger.warning(f"Error fetching Kraken {crypto_symbol} price: {e}") supported_pairs_cache[f"kraken:{cache_key}"] = False
logger.debug(f"Kraken does not support {crypto_symbol}/{currency_code} pair: {e}")
prices['kraken'] = None prices['kraken'] = None
else: else:
prices['kraken'] = None prices['kraken'] = None
# Calculate average # Calculate average only if we have valid prices
if enabled_prices: if enabled_prices:
prices['average'] = sum(enabled_prices) / len(enabled_prices) prices['average'] = sum(enabled_prices) / len(enabled_prices)
else: else:
...@@ -7980,18 +8132,27 @@ async def dashboard_wallet(request: Request): ...@@ -7980,18 +8132,27 @@ async def dashboard_wallet(request: Request):
auth_check = require_dashboard_auth(request) auth_check = require_dashboard_auth(request)
if auth_check: if auth_check:
return auth_check return auth_check
db = DatabaseRegistry.get_config_database() try:
user_id = request.session.get('user_id') db = DatabaseRegistry.get_config_database()
user_id = request.session.get('user_id')
from aisbf.payments.wallet.manager import WalletManager
wallet_manager = WalletManager(db) from aisbf.payments.wallet.manager import WalletManager
wallet = await wallet_manager.get_wallet(user_id) wallet_manager = WalletManager(db)
wallet = await wallet_manager.get_wallet(user_id)
return templates.TemplateResponse("dashboard/wallet.html", {
"request": request, return templates.TemplateResponse("dashboard/wallet.html", {
"wallet": wallet "request": request,
}) "wallet": wallet
})
except ImportError:
return HTMLResponse("Wallet functionality not available", status_code=503)
except Exception as e:
logger.error(f"Failed to load wallet page: {e}")
return templates.TemplateResponse("dashboard/error.html", {
"request": request,
"error": "Failed to load wallet. Please try again later."
}, status_code=500)
@app.get("/dashboard/billing") @app.get("/dashboard/billing")
async def dashboard_billing(request: Request): async def dashboard_billing(request: Request):
...@@ -8016,6 +8177,11 @@ async def dashboard_billing(request: Request): ...@@ -8016,6 +8177,11 @@ async def dashboard_billing(request: Request):
if settings.get('enabled', False): if settings.get('enabled', False):
enabled_gateways.append(gateway) enabled_gateways.append(gateway)
# Get user wallet
currency_settings = db.get_currency_settings()
currency_code = currency_settings.get('currency_code', 'EUR')
wallet = db.get_user_wallet(user_id) or {'balance': '0.00', 'currency_code': currency_code, 'auto_topup_enabled': False}
return templates.TemplateResponse( return templates.TemplateResponse(
request=request, request=request,
name="dashboard/billing.html", name="dashboard/billing.html",
...@@ -8024,7 +8190,8 @@ async def dashboard_billing(request: Request): ...@@ -8024,7 +8190,8 @@ async def dashboard_billing(request: Request):
"session": request.session, "session": request.session,
"payment_methods": payment_methods, "payment_methods": payment_methods,
"transactions": transactions, "transactions": transactions,
"enabled_gateways": enabled_gateways "enabled_gateways": enabled_gateways,
"wallet": wallet
} }
) )
......
...@@ -28,7 +28,7 @@ keywords = [ ...@@ -28,7 +28,7 @@ keywords = [
"broker", "broker",
] ]
classifiers = [ classifiers = [
"Development Status :: 3 - Alpha", "Development Status :: 4 - Beta",
"Intended Audience :: Developers", "Intended Audience :: Developers",
"Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Software Development :: Libraries :: Python Modules",
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",
......
...@@ -58,7 +58,7 @@ setup( ...@@ -58,7 +58,7 @@ setup(
url="https://git.nexlab.net/nexlab/aisbf.git", url="https://git.nexlab.net/nexlab/aisbf.git",
packages=find_packages(), packages=find_packages(),
classifiers=[ classifiers=[
"Development Status :: 3 - Alpha", "Development Status :: 4 - Beta",
"Intended Audience :: Developers", "Intended Audience :: Developers",
"Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Software Development :: Libraries :: Python Modules",
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
...@@ -184,6 +184,12 @@ setup( ...@@ -184,6 +184,12 @@ setup(
'aisbf/payments/notifications/__init__.py', 'aisbf/payments/notifications/__init__.py',
'aisbf/payments/notifications/email.py', 'aisbf/payments/notifications/email.py',
]), ]),
# aisbf.payments.wallet subpackage
('share/aisbf/aisbf/payments/wallet', [
'aisbf/payments/wallet/__init__.py',
'aisbf/payments/wallet/manager.py',
'aisbf/payments/wallet/routes.py',
]),
# Install dashboard templates # Install dashboard templates
('share/aisbf/templates', [ ('share/aisbf/templates', [
'templates/base.html', 'templates/base.html',
...@@ -259,4 +265,4 @@ setup( ...@@ -259,4 +265,4 @@ setup(
cmdclass={ cmdclass={
'install': InstallCommand, 'install': InstallCommand,
}, },
) )
\ No newline at end of file
...@@ -504,6 +504,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -504,6 +504,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<a href="{{ url_for(request, '/dashboard/analytics') }}" {% if '/analytics' in request.path %}class="active"{% endif %}>Analytics</a> <a href="{{ url_for(request, '/dashboard/analytics') }}" {% if '/analytics' in request.path %}class="active"{% endif %}>Analytics</a>
{% if request.session.user_id %} {% if request.session.user_id %}
<a href="{{ url_for(request, '/dashboard/user/tokens') }}" {% if '/user/tokens' in request.path %}class="active"{% endif %}>API Tokens</a> <a href="{{ url_for(request, '/dashboard/user/tokens') }}" {% if '/user/tokens' in request.path %}class="active"{% endif %}>API Tokens</a>
<a href="{{ url_for(request, '/dashboard/wallet') }}" {% if '/wallet' in request.path %}class="active"{% endif %}>Wallet</a>
{% endif %} {% endif %}
{% if request.session.role == 'admin' %} {% if request.session.role == 'admin' %}
<a href="{{ url_for(request, '/dashboard/users') }}" {% if '/users' in request.path %}class="active"{% endif %}>Users</a> <a href="{{ url_for(request, '/dashboard/users') }}" {% if '/users' in request.path %}class="active"{% endif %}>Users</a>
...@@ -527,6 +528,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -527,6 +528,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<a href="{{ url_for(request, '/dashboard/user/tokens') }}">API Tokens</a> <a href="{{ url_for(request, '/dashboard/user/tokens') }}">API Tokens</a>
<a href="{{ url_for(request, '/dashboard/cache-settings') }}">Cache Settings</a> <a href="{{ url_for(request, '/dashboard/cache-settings') }}">Cache Settings</a>
<a href="{{ url_for(request, '/dashboard/subscription') }}">Subscription</a> <a href="{{ url_for(request, '/dashboard/subscription') }}">Subscription</a>
<a href="{{ url_for(request, '/dashboard/wallet') }}">Wallet</a>
<a href="{{ url_for(request, '/dashboard/billing') }}">Billing</a> <a href="{{ url_for(request, '/dashboard/billing') }}">Billing</a>
<a href="{{ url_for(request, '/dashboard/change-password') }}">Change Password</a> <a href="{{ url_for(request, '/dashboard/change-password') }}">Change Password</a>
{% endif %} {% endif %}
......
...@@ -6,7 +6,13 @@ ...@@ -6,7 +6,13 @@
<h2 style="margin-bottom: 30px;">Add Payment Method</h2> <h2 style="margin-bottom: 30px;">Add Payment Method</h2>
<div style="background: #16213e; border: 2px solid #4a9eff; border-radius: 8px; padding: 30px; margin-bottom: 20px;"> <div style="background: #16213e; border: 2px solid #4a9eff; border-radius: 8px; padding: 30px; margin-bottom: 20px;">
<p style="color: #a0a0a0; margin-bottom: 30px;">Choose a payment method to add to your account. You can use this for subscription payments and plan upgrades.</p> <p style="color: #a0a0a0; margin-bottom: 10px;">Choose a payment method to add to your account.</p>
<div style="background: #1a1a2e; border-left: 3px solid #4ade80; padding: 15px; margin-bottom: 30px; border-radius: 4px;">
<p style="margin: 0; color: #e0e0e0;">
<i class="fas fa-info-circle me-2 text-success"></i>
<strong>Important:</strong> Only <strong>Credit Card (Stripe)</strong> can be used for automatic subscription renewals. All other payment methods are for wallet top-ups only.
</p>
</div>
<!-- Stripe and PayPal --> <!-- Stripe and PayPal -->
{% if 'stripe' in enabled_gateways or 'paypal' in enabled_gateways %} {% if 'stripe' in enabled_gateways or 'paypal' in enabled_gateways %}
......
...@@ -9,6 +9,16 @@ ...@@ -9,6 +9,16 @@
<i class="fas fa-cog me-2"></i>Payment System Settings <i class="fas fa-cog me-2"></i>Payment System Settings
</h2> </h2>
<!-- Currency Selection Warning -->
<div style="background: #1a1a2e; border-left: 4px solid #f39c12; padding: 15px; margin-bottom: 25px; border-radius: 4px;">
<div style="color: #f39c12; font-weight: bold; margin-bottom: 8px;">
<i class="fas fa-info-circle me-2"></i>Important Note About Currency Selection
</div>
<div style="color: #e0e0e0; font-size: 14px;">
We do not suggest using USD if you are not in the USA. This project does not support the actions of the USA in the international context and we don't support a politic of war and threats to allies.
</div>
</div>
<!-- Encryption Key Configuration --> <!-- Encryption Key Configuration -->
<div style="background: #16213e; border: 2px solid #e74c3c; border-radius: 8px; padding: 20px; margin-bottom: 20px;"> <div style="background: #16213e; border: 2px solid #e74c3c; border-radius: 8px; padding: 20px; margin-bottom: 20px;">
<h3 style="margin: 0 0 20px 0; color: #e74c3c;"> <h3 style="margin: 0 0 20px 0; color: #e74c3c;">
......
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block content %}
<h2 style="margin-bottom: 30px;">Billing & Payments</h2> <h2 style="margin-bottom: 20px;">Billing & Payments</h2>
<!-- Wallet Balance Banner -->
<div style="background: linear-gradient(135deg, #1a4a2e, #0f3460); border: 2px solid #28a745; border-radius: 8px; padding: 20px; margin-bottom: 20px;">
<div style="display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 15px;">
<div>
<div style="color: #a0a0a0; font-size: 14px; margin-bottom: 5px;">Wallet Balance</div>
<div style="font-size: 32px; font-weight: bold; color: #4ade80;">${{ wallet.balance }}</div>
<div style="color: #a0a0a0; font-size: 13px; margin-top: 5px;">
All subscription renewals and payments are automatically charged from your wallet first.
</div>
</div>
<a href="{{ url_for(request, '/dashboard/wallet') }}" class="btn" style="background: #28a745;">
<i class="fas fa-wallet me-2"></i>Manage Wallet
</a>
</div>
</div>
{% if success %} {% if success %}
<div class="alert alert-success">{{ success }}</div> <div class="alert alert-success">{{ success }}</div>
......
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