Fix database creations

parent d5565050
...@@ -1898,7 +1898,7 @@ class DatabaseManager: ...@@ -1898,7 +1898,7 @@ class DatabaseManager:
conn.commit() conn.commit()
# User API token methods # User API token methods
def create_user_api_token(self, user_id: int, token: str, description: str = None) -> int: def create_user_api_token(self, user_id: int, token: str, description: str = None, scope: str = 'api') -> int:
""" """
Create a new API token for a user. Create a new API token for a user.
...@@ -1906,17 +1906,20 @@ class DatabaseManager: ...@@ -1906,17 +1906,20 @@ class DatabaseManager:
user_id: User ID user_id: User ID
token: The token string token: The token string
description: Optional description description: Optional description
scope: Token scope - 'api' (proxy only), 'mcp' (MCP only), or 'both'
Returns: Returns:
Token ID Token ID
""" """
if scope not in ('api', 'mcp', 'both'):
scope = 'api'
with self._get_connection() as conn: with self._get_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
placeholder = '?' if self.db_type == 'sqlite' else '%s' placeholder = '?' if self.db_type == 'sqlite' else '%s'
cursor.execute(f''' cursor.execute(f'''
INSERT INTO user_api_tokens (user_id, token, description) INSERT INTO user_api_tokens (user_id, token, description, scope)
VALUES ({placeholder}, {placeholder}, {placeholder}) VALUES ({placeholder}, {placeholder}, {placeholder}, {placeholder})
''', (user_id, token, description)) ''', (user_id, token, description, scope))
conn.commit() conn.commit()
return cursor.lastrowid return cursor.lastrowid
...@@ -1934,7 +1937,8 @@ class DatabaseManager: ...@@ -1934,7 +1937,8 @@ class DatabaseManager:
cursor = conn.cursor() cursor = conn.cursor()
placeholder = '?' if self.db_type == 'sqlite' else '%s' placeholder = '?' if self.db_type == 'sqlite' else '%s'
cursor.execute(f''' cursor.execute(f'''
SELECT id, token, description, created_at, last_used, is_active SELECT id, token, description, created_at, last_used, is_active,
COALESCE(scope, 'api') as scope
FROM user_api_tokens FROM user_api_tokens
WHERE user_id = {placeholder} WHERE user_id = {placeholder}
ORDER BY created_at DESC ORDER BY created_at DESC
...@@ -1948,7 +1952,8 @@ class DatabaseManager: ...@@ -1948,7 +1952,8 @@ class DatabaseManager:
'description': row[2], 'description': row[2],
'created_at': row[3], 'created_at': row[3],
'last_used': row[4], 'last_used': row[4],
'is_active': row[5] 'is_active': row[5],
'scope': row[6]
}) })
return tokens return tokens
...@@ -1966,7 +1971,8 @@ class DatabaseManager: ...@@ -1966,7 +1971,8 @@ class DatabaseManager:
cursor = conn.cursor() cursor = conn.cursor()
placeholder = '?' if self.db_type == 'sqlite' else '%s' placeholder = '?' if self.db_type == 'sqlite' else '%s'
cursor.execute(f''' cursor.execute(f'''
SELECT u.id, u.username, u.role, t.id as token_id SELECT u.id, u.username, u.role, t.id as token_id,
COALESCE(t.scope, 'api') as scope
FROM users u FROM users u
JOIN user_api_tokens t ON u.id = t.user_id JOIN user_api_tokens t ON u.id = t.user_id
WHERE t.token = {placeholder} AND t.is_active = 1 AND u.is_active = 1 WHERE t.token = {placeholder} AND t.is_active = 1 AND u.is_active = 1
...@@ -1978,7 +1984,8 @@ class DatabaseManager: ...@@ -1978,7 +1984,8 @@ class DatabaseManager:
'user_id': row[0], 'user_id': row[0],
'username': row[1], 'username': row[1],
'role': row[2], 'role': row[2],
'token_id': row[3] 'token_id': row[3],
'scope': row[4]
} }
return None return None
...@@ -3382,11 +3389,7 @@ def DatabaseManager__initialize_database(self): ...@@ -3382,11 +3389,7 @@ def DatabaseManager__initialize_database(self):
cursor = conn.cursor() cursor = conn.cursor()
if self.db_type == 'sqlite': if self.db_type == 'sqlite':
# Enable WAL mode for better concurrent access
# WAL allows multiple readers and one writer simultaneously
cursor.execute('PRAGMA journal_mode=WAL') cursor.execute('PRAGMA journal_mode=WAL')
# Set busy timeout to 5 seconds for concurrent access
cursor.execute('PRAGMA busy_timeout=5000') cursor.execute('PRAGMA busy_timeout=5000')
auto_increment = 'AUTOINCREMENT' auto_increment = 'AUTOINCREMENT'
timestamp_default = 'CURRENT_TIMESTAMP' timestamp_default = 'CURRENT_TIMESTAMP'
...@@ -3396,27 +3399,7 @@ def DatabaseManager__initialize_database(self): ...@@ -3396,27 +3399,7 @@ def DatabaseManager__initialize_database(self):
timestamp_default = 'CURRENT_TIMESTAMP' timestamp_default = 'CURRENT_TIMESTAMP'
boolean_type = 'TINYINT(1)' boolean_type = 'TINYINT(1)'
# ==============================================
# SAFETY CHECK: NEVER CREATE USER TABLES IN CACHE DB
# ==============================================
if self.database_type == DatabaseRegistry.TYPE_CONFIG: if self.database_type == DatabaseRegistry.TYPE_CONFIG:
# ONLY CREATE CONFIG TABLES IN CONFIG DATABASE
# Create context_dimensions table for tracking context usage
# cursor.execute(f'''
# CREATE TABLE IF NOT EXISTS context_dimensions (
# id INTEGER PRIMARY KEY {auto_increment},
# provider_id VARCHAR(255) NOT NULL,
# model_name VARCHAR(255) NOT NULL,
# context_size INTEGER,
# condense_context INTEGER,
# condense_method TEXT,
# effective_context INTEGER DEFAULT 0,
# last_updated TIMESTAMP DEFAULT {timestamp_default},
# UNIQUE(provider_id, model_name)
# )
# ''')
#
# Create token_usage table for tracking rate limiting
cursor.execute(f''' cursor.execute(f'''
CREATE TABLE IF NOT EXISTS token_usage ( CREATE TABLE IF NOT EXISTS token_usage (
id INTEGER PRIMARY KEY {auto_increment}, id INTEGER PRIMARY KEY {auto_increment},
...@@ -3435,612 +3418,81 @@ def DatabaseManager__initialize_database(self): ...@@ -3435,612 +3418,81 @@ def DatabaseManager__initialize_database(self):
) )
''') ''')
# Migration: Add missing columns to token_usage table # Migration: add columns to token_usage for older databases
try: try:
if self.db_type == 'sqlite': if self.db_type == 'sqlite':
cursor.execute("PRAGMA table_info(token_usage)") cursor.execute("PRAGMA table_info(token_usage)")
columns = [row[1] for row in cursor.fetchall()] columns = [row[1] for row in cursor.fetchall()]
if 'prompt_tokens' not in columns: for col, defn in [
cursor.execute('ALTER TABLE token_usage ADD COLUMN prompt_tokens INTEGER') ('prompt_tokens', 'INTEGER'),
logger.info("✅ Migration: Added prompt_tokens column to token_usage") ('completion_tokens', 'INTEGER'),
if 'completion_tokens' not in columns: ('actual_cost', 'DECIMAL(10,6)'),
cursor.execute('ALTER TABLE token_usage ADD COLUMN completion_tokens INTEGER') ('success', 'BOOLEAN DEFAULT 1'),
logger.info("✅ Migration: Added completion_tokens column to token_usage") ('latency_ms', 'INTEGER'),
if 'actual_cost' not in columns: ('error_type', 'VARCHAR(255)'),
cursor.execute('ALTER TABLE token_usage ADD COLUMN actual_cost DECIMAL(10,6)') ('token_id', 'INTEGER'),
logger.info("✅ Migration: Added actual_cost column to token_usage") ]:
if 'success' not in columns: if col not in columns:
cursor.execute('ALTER TABLE token_usage ADD COLUMN success BOOLEAN DEFAULT 1') cursor.execute(f'ALTER TABLE token_usage ADD COLUMN {col} {defn}')
logger.info("✅ Migration: Added success column to token_usage") logger.info(f"✅ Migration: Added {col} column to token_usage")
if 'latency_ms' not in columns: else:
cursor.execute('ALTER TABLE token_usage ADD COLUMN latency_ms INTEGER')
logger.info("✅ Migration: Added latency_ms column to token_usage")
if 'error_type' not in columns:
cursor.execute('ALTER TABLE token_usage ADD COLUMN error_type VARCHAR(255)')
logger.info("✅ Migration: Added error_type column to token_usage")
if 'token_id' not in columns:
cursor.execute('ALTER TABLE token_usage ADD COLUMN token_id INTEGER')
logger.info("✅ Migration: Added token_id column to token_usage")
else: # mysql
# Check for prompt_tokens column
cursor.execute("""
SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'token_usage' AND COLUMN_NAME = 'prompt_tokens'
""")
if not cursor.fetchone():
cursor.execute('ALTER TABLE token_usage ADD COLUMN prompt_tokens INTEGER')
logger.info("✅ Migration: Added prompt_tokens column to token_usage")
# Check for completion_tokens column
cursor.execute("""
SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'token_usage' AND COLUMN_NAME = 'completion_tokens'
""")
if not cursor.fetchone():
cursor.execute('ALTER TABLE token_usage ADD COLUMN completion_tokens INTEGER')
logger.info("✅ Migration: Added completion_tokens column to token_usage")
# Check for actual_cost column
cursor.execute("""
SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'token_usage' AND COLUMN_NAME = 'actual_cost'
""")
if not cursor.fetchone():
cursor.execute('ALTER TABLE token_usage ADD COLUMN actual_cost DECIMAL(10,6)')
logger.info("✅ Migration: Added actual_cost column to token_usage")
# Check for success column
cursor.execute(""" cursor.execute("""
SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'token_usage' AND COLUMN_NAME = 'success' WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'token_usage'
""") """)
if not cursor.fetchone(): existing = {row[0] for row in cursor.fetchall()}
cursor.execute('ALTER TABLE token_usage ADD COLUMN success BOOLEAN DEFAULT 1') for col, defn in [
logger.info("✅ Migration: Added success column to token_usage") ('prompt_tokens', 'INTEGER'),
('completion_tokens', 'INTEGER'),
('actual_cost', 'DECIMAL(10,6)'),
('success', 'BOOLEAN DEFAULT 1'),
('latency_ms', 'INTEGER'),
('error_type', 'VARCHAR(255)'),
('token_id', 'INTEGER'),
]:
if col not in existing:
cursor.execute(f'ALTER TABLE token_usage ADD COLUMN {col} {defn}')
logger.info(f"✅ Migration: Added {col} column to token_usage")
except Exception as e:
logger.warning(f"Migration check for token_usage columns: {e}")
# Check for latency_ms column cursor.execute(f'''
cursor.execute(""" CREATE TABLE IF NOT EXISTS admin_settings (
SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS id INTEGER PRIMARY KEY {auto_increment},
WHERE TABLE_NAME = 'token_usage' AND COLUMN_NAME = 'latency_ms' setting_key VARCHAR(255) UNIQUE NOT NULL,
""") setting_value TEXT,
if not cursor.fetchone(): updated_at TIMESTAMP DEFAULT {timestamp_default}
cursor.execute('ALTER TABLE token_usage ADD COLUMN latency_ms INTEGER') )
logger.info("✅ Migration: Added latency_ms column to token_usage") ''')
# Check for error_type column cursor.execute(f'''
cursor.execute(""" CREATE TABLE IF NOT EXISTS users (
SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS id INTEGER PRIMARY KEY {auto_increment},
WHERE TABLE_NAME = 'token_usage' AND COLUMN_NAME = 'error_type' username VARCHAR(255) UNIQUE NOT NULL,
""") email VARCHAR(255) UNIQUE,
if not cursor.fetchone(): display_name VARCHAR(255),
cursor.execute('ALTER TABLE token_usage ADD COLUMN error_type VARCHAR(255)') password_hash VARCHAR(255) NOT NULL,
logger.info("✅ Migration: Added error_type column to token_usage") role VARCHAR(50) DEFAULT 'user',
created_by VARCHAR(255),
created_at TIMESTAMP DEFAULT {timestamp_default},
last_login TIMESTAMP NULL,
is_active {boolean_type} DEFAULT 1,
email_verified {boolean_type} DEFAULT 0,
verification_token VARCHAR(255),
verification_token_expires TIMESTAMP NULL,
last_verification_email_sent TIMESTAMP NULL
)
''')
# Check for token_id column self._run_config_migrations(cursor, auto_increment, timestamp_default, boolean_type)
cursor.execute("""
SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'token_usage' AND COLUMN_NAME = 'token_id'
""")
if not cursor.fetchone():
cursor.execute('ALTER TABLE token_usage ADD COLUMN token_id INTEGER')
logger.info("✅ Migration: Added token_id column to token_usage")
except Exception as e:
logger.warning(f"Migration check for token_usage columns: {e}")
# else:
self._create_cache_tables(cursor, auto_increment, timestamp_default, boolean_type)
#
#
#
# Create indexes for better query performance
# try:
# cursor.execute('''
# CREATE INDEX IF NOT EXISTS idx_context_provider_model
# ON context_dimensions(provider_id, model_name)
# ''')
# except:
# pass # Index might already exist
#
# try:
# cursor.execute('''
# CREATE INDEX IF NOT EXISTS idx_token_provider_model
# ON token_usage(provider_id, model_name)
# ''')
# except:
# pass
#
# try:
# cursor.execute('''
# CREATE INDEX IF NOT EXISTS idx_token_timestamp
# ON token_usage(timestamp)
# ''')
# except:
# pass
#
# Create model_embeddings table for caching vectorized model descriptions
# cursor.execute(f'''
# CREATE TABLE IF NOT EXISTS model_embeddings (
# id INTEGER PRIMARY KEY {auto_increment},
# provider_id VARCHAR(255) NOT NULL,
# model_name VARCHAR(255) NOT NULL,
# description TEXT,
# embedding TEXT,
# last_updated TIMESTAMP DEFAULT {timestamp_default},
# UNIQUE(provider_id, model_name)
# )
# ''')
#
# try:
# # Index creation moved to separate migration
#
# Create admin settings table for system configuration
cursor.execute(f'''
CREATE TABLE IF NOT EXISTS admin_settings (
id INTEGER PRIMARY KEY {auto_increment},
setting_key VARCHAR(255) UNIQUE NOT NULL,
setting_value TEXT,
updated_at TIMESTAMP DEFAULT {timestamp_default}
)
''')
# Create users table for multi-user management
cursor.execute(f'''
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY {auto_increment},
username VARCHAR(255) UNIQUE NOT NULL,
email VARCHAR(255) UNIQUE,
display_name VARCHAR(255),
password_hash VARCHAR(255) NOT NULL,
role VARCHAR(50) DEFAULT 'user',
created_by VARCHAR(255),
created_at TIMESTAMP DEFAULT {timestamp_default},
last_login TIMESTAMP NULL,
is_active {boolean_type} DEFAULT 1,
email_verified {boolean_type} DEFAULT 0,
verification_token VARCHAR(255),
verification_token_expires TIMESTAMP NULL,
last_verification_email_sent TIMESTAMP NULL
)
''')
#
# Migration: Add display_name column if it doesn't exist
# try:
# Check if display_name column exists
# if self.db_type == 'sqlite':
# cursor.execute("PRAGMA table_info(users)")
# columns = [row[1] for row in cursor.fetchall()]
# else:
# cursor.execute("""
# SELECT COLUMN_NAME
# FROM INFORMATION_SCHEMA.COLUMNS
# WHERE TABLE_NAME = 'users'
# """)
# columns = [row[0] for row in cursor.fetchall()]
#
# if 'display_name' not in columns:
# logger.info("Adding display_name column to users table")
# cursor.execute("ALTER TABLE users ADD COLUMN display_name VARCHAR(255)")
# conn.commit()
#
# Populate display_name for existing users
# cursor.execute("UPDATE users SET display_name = username WHERE display_name IS NULL")
# conn.commit()
# logger.info("Migration complete: display_name column added and populated")
# except Exception as e:
# logger.warning(f"Migration warning (display_name): {e}")
#
# User-specific configuration tables for multi-user isolation - commented out to fix import
# cursor.execute(f'''
# CREATE TABLE IF NOT EXISTS user_providers (
# id INTEGER PRIMARY KEY {auto_increment},
# user_id INTEGER NOT NULL,
# provider_id VARCHAR(255) NOT NULL,
# config TEXT NOT NULL,
# created_at TIMESTAMP DEFAULT {timestamp_default},
# updated_at TIMESTAMP DEFAULT {timestamp_default},
# FOREIGN KEY (user_id) REFERENCES users(id),
# UNIQUE(user_id, provider_id)
# )
# ''')
#
# cursor.execute(f'''
# CREATE TABLE IF NOT EXISTS user_rotations (
# id INTEGER PRIMARY KEY {auto_increment},
# user_id INTEGER NOT NULL,
# rotation_id VARCHAR(255) NOT NULL,
# config TEXT NOT NULL,
# created_at TIMESTAMP DEFAULT {timestamp_default},
# updated_at TIMESTAMP DEFAULT {timestamp_default},
# FOREIGN KEY (user_id) REFERENCES users(id),
# UNIQUE(user_id, rotation_id)
# )
# ''')
#
# cursor.execute(f'''
# CREATE TABLE IF NOT EXISTS user_autoselects (
# id INTEGER PRIMARY KEY {auto_increment},
# user_id INTEGER NOT NULL,
# autoselect_id VARCHAR(255) NOT NULL,
# config TEXT NOT NULL,
# created_at TIMESTAMP DEFAULT {timestamp_default},
# updated_at TIMESTAMP DEFAULT {timestamp_default},
# FOREIGN KEY (user_id) REFERENCES users(id),
# UNIQUE(user_id, autoselect_id)
# )
# ''')
#
# cursor.execute(f'''
# CREATE TABLE IF NOT EXISTS user_prompts (
# id INTEGER PRIMARY KEY {auto_increment},
# user_id INTEGER NOT NULL,
# prompt_key VARCHAR(255) NOT NULL,
# content TEXT NOT NULL,
# created_at TIMESTAMP DEFAULT {timestamp_default},
# updated_at TIMESTAMP DEFAULT {timestamp_default},
# FOREIGN KEY (user_id) REFERENCES users(id),
# UNIQUE(user_id, prompt_key)
# )
# ''')
#
# cursor.execute(f'''
# CREATE TABLE IF NOT EXISTS user_api_tokens (
# id INTEGER PRIMARY KEY {auto_increment},
# user_id INTEGER NOT NULL,
# token VARCHAR(255) UNIQUE NOT NULL,
# description TEXT,
# created_at TIMESTAMP DEFAULT {timestamp_default},
# last_used TIMESTAMP NULL,
# is_active {boolean_type} DEFAULT 1,
# FOREIGN KEY (user_id) REFERENCES users(id)
# )
# ''')
#
# cursor.execute(f'''
# CREATE TABLE IF NOT EXISTS user_token_usage (
# id INTEGER PRIMARY KEY {auto_increment},
# user_id INTEGER NOT NULL,
# token_id INTEGER,
# provider_id VARCHAR(255) NOT NULL,
# model_name VARCHAR(255) NOT NULL,
# tokens_used INTEGER NOT NULL,
# timestamp TIMESTAMP DEFAULT {timestamp_default},
# FOREIGN KEY (user_id) REFERENCES users(id),
# FOREIGN KEY (token_id) REFERENCES user_api_tokens(id)
# )
# ''')
#
# Create user_auth_files table for storing authentication file metadata
# cursor.execute(f'''
# CREATE TABLE IF NOT EXISTS user_auth_files (
# id INTEGER PRIMARY KEY {auto_increment},
# user_id INTEGER NOT NULL,
# provider_id VARCHAR(255) NOT NULL,
# file_type VARCHAR(50) NOT NULL,
# original_filename VARCHAR(255) NOT NULL,
# stored_filename VARCHAR(255) NOT NULL,
# file_path TEXT NOT NULL,
# file_size INTEGER,
# mime_type VARCHAR(100),
# created_at TIMESTAMP DEFAULT {timestamp_default},
# updated_at TIMESTAMP DEFAULT {timestamp_default},
# FOREIGN KEY (user_id) REFERENCES users(id),
# UNIQUE(user_id, provider_id, file_type)
# )
# ''')
#
# Create user_oauth2_credentials table for storing OAuth2 tokens per user/provider
# cursor.execute(f'''
# CREATE TABLE IF NOT EXISTS user_oauth2_credentials (
# id INTEGER PRIMARY KEY {auto_increment},
# user_id INTEGER NOT NULL,
# provider_id VARCHAR(255) NOT NULL,
# auth_type VARCHAR(50) NOT NULL,
# credentials TEXT NOT NULL,
# created_at TIMESTAMP DEFAULT {timestamp_default},
# updated_at TIMESTAMP DEFAULT {timestamp_default},
# FOREIGN KEY (user_id) REFERENCES users(id),
# UNIQUE(user_id, provider_id, auth_type)
# )
# ''')
#
# ==============================================
# UNIVERSAL MIGRATIONS - RUN ON EVERY STARTUP
# ==============================================
# logger.info("Running database migrations...")
#
# Migration: Create account_tiers table if missing
# try:
# if self.db_type == 'sqlite':
# cursor.execute("PRAGMA table_info(account_tiers)")
# if not cursor.fetchall():
# cursor.execute(f'''
# CREATE TABLE account_tiers (
# id INTEGER PRIMARY KEY {auto_increment},
# name VARCHAR(255) UNIQUE NOT NULL,
# description TEXT,
# price_monthly DECIMAL(10,2) DEFAULT 0.00,
# price_yearly DECIMAL(10,2) DEFAULT 0.00,
# is_default {boolean_type} DEFAULT 0,
# is_active {boolean_type} DEFAULT 1,
# max_requests_per_day INTEGER DEFAULT -1,
# max_requests_per_month INTEGER DEFAULT -1,
# max_providers INTEGER DEFAULT -1,
# max_rotations INTEGER DEFAULT -1,
# max_autoselections INTEGER DEFAULT -1,
# max_rotation_models INTEGER DEFAULT -1,
# max_autoselection_models INTEGER DEFAULT -1,
# created_at TIMESTAMP DEFAULT {timestamp_default},
# updated_at TIMESTAMP DEFAULT {timestamp_default}
# )
# ''')
# conn.commit()
# logger.info("✅ Migration: Created missing account_tiers table")
# except Exception as e:
# logger.warning(f"Migration check for account_tiers table: {e}")
#
# Migration: Add missing columns to account_tiers
# try:
# if self.db_type == 'sqlite':
# cursor.execute("PRAGMA table_info(account_tiers)")
# existing_columns = [row[1] for row in cursor.fetchall()]
# tier_columns = [
# ('max_requests_per_day', 'INTEGER DEFAULT -1'),
# ('max_requests_per_month', 'INTEGER DEFAULT -1'),
# ('max_providers', 'INTEGER DEFAULT -1'),
# ('max_rotations', 'INTEGER DEFAULT -1'),
# ('max_autoselections', 'INTEGER DEFAULT -1'),
# ('max_rotation_models', 'INTEGER DEFAULT -1'),
# ('max_autoselection_models', 'INTEGER DEFAULT -1'),
# ('is_default', f'{boolean_type} DEFAULT 0'),
# ('is_active', f'{boolean_type} DEFAULT 1'),
# ('is_visible', f'{boolean_type} DEFAULT 1')
# ]
# col_count = 0
# for col_name, col_def in tier_columns:
# if col_name not in existing_columns:
# cursor.execute(f'ALTER TABLE account_tiers ADD COLUMN {col_name} {col_def}')
# col_count += 1
# if col_count > 0:
# logger.info(f"✅ Migration: Added {col_count} missing columns to account_tiers")
# else:
# MySQL/MariaDB
# cursor.execute("""
# SELECT COLUMN_NAME
# FROM INFORMATION_SCHEMA.COLUMNS
# WHERE TABLE_NAME = 'account_tiers'
# AND TABLE_SCHEMA = DATABASE()
# """)
# existing_columns = [row[0] for row in cursor.fetchall()]
# tier_columns = [
# ('max_requests_per_day', 'INTEGER DEFAULT -1'),
# ('max_requests_per_month', 'INTEGER DEFAULT -1'),
# ('max_providers', 'INTEGER DEFAULT -1'),
# ('max_rotations', 'INTEGER DEFAULT -1'),
# ('max_autoselections', 'INTEGER DEFAULT -1'),
# ('max_rotation_models', 'INTEGER DEFAULT -1'),
# ('max_autoselection_models', 'INTEGER DEFAULT -1'),
# ('is_default', f'{boolean_type} DEFAULT 0'),
# ('is_active', f'{boolean_type} DEFAULT 1'),
# ('is_visible', f'{boolean_type} DEFAULT 1')
# ]
# col_count = 0
# for col_name, col_def in tier_columns:
# if col_name not in existing_columns:
# cursor.execute(f'ALTER TABLE account_tiers ADD COLUMN {col_name} {col_def}')
# col_count += 1
# if col_count > 0:
# conn.commit()
# logger.info(f"✅ Migration: Added {col_count} missing columns to account_tiers")
# except Exception as e:
# logger.warning(f"Migration check for account_tiers columns: {e}")
#
# Migration: Ensure default free tier exists
# try:
# cursor.execute(f'SELECT COUNT(*) FROM account_tiers WHERE is_default = 1')
# free_tier_count = cursor.fetchone()[0]
# if free_tier_count == 0:
# cursor.execute(f'''
# INSERT INTO account_tiers
# (name, description, price_monthly, price_yearly, is_default, is_active,
# max_requests_per_day, max_requests_per_month, max_providers, max_rotations,
# max_autoselections, max_rotation_models, max_autoselection_models)
# VALUES
# ('Free Tier', 'Default free account tier with unlimited access', 0.00, 0.00, 1, 1,
# -1, -1, -1, -1, -1, -1, -1)
# ''')
# logger.info("✅ Migration: Inserted default free tier")
# except Exception as e:
# logger.warning(f"Migration check for default free tier: {e}")
#
# Migration: Add tier_id column to users table
# try:
# if self.db_type == 'sqlite':
# cursor.execute("PRAGMA table_info(users)")
# 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')
# cursor.execute('ALTER TABLE users ADD COLUMN subscription_expires TIMESTAMP NULL')
# logger.info("✅ Migration: Added tier_id and subscription_expires columns to users")
# else:
# cursor.execute("""
# SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS
# 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:
# logger.warning(f"Migration check for users.tier_id: {e}")
#
# Migration: Add password reset token columns to users table
# try:
# if self.db_type == 'sqlite':
# cursor.execute("PRAGMA table_info(users)")
# columns = [row[1] for row in cursor.fetchall()]
# if 'reset_password_token' not in columns:
# cursor.execute('ALTER TABLE users ADD COLUMN reset_password_token VARCHAR(255)')
# cursor.execute('ALTER TABLE users ADD COLUMN reset_password_token_expires TIMESTAMP NULL')
# logger.info("✅ Migration: Added password reset token columns to users")
# else:
# cursor.execute("""
# SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS
# WHERE TABLE_NAME = 'users' AND COLUMN_NAME = 'reset_password_token'
# """)
# if not cursor.fetchone():
# cursor.execute('ALTER TABLE users ADD COLUMN reset_password_token VARCHAR(255)')
# cursor.execute('ALTER TABLE users ADD COLUMN reset_password_token_expires TIMESTAMP NULL')
# logger.info("✅ Migration: Added password reset token columns to users")
# except Exception as e:
# logger.warning(f"Migration check for users.reset_password_token: {e}")
#
# Migration: Add last_verification_email_sent column to users table
# try:
# if self.db_type == 'sqlite':
# cursor.execute("PRAGMA table_info(users)")
# columns = [row[1] for row in cursor.fetchall()]
# if 'last_verification_email_sent' not in columns:
# cursor.execute('ALTER TABLE users ADD COLUMN last_verification_email_sent TIMESTAMP NULL')
# logger.info("✅ Migration: Added last_verification_email_sent column to users")
# else:
# cursor.execute("""
# SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS
# WHERE TABLE_NAME = 'users' AND COLUMN_NAME = 'last_verification_email_sent'
# """)
# if not cursor.fetchone():
# cursor.execute('ALTER TABLE users ADD COLUMN last_verification_email_sent TIMESTAMP NULL')
# logger.info("✅ Migration: Added last_verification_email_sent column to users")
# except Exception as e:
# logger.warning(f"Migration check for users.last_verification_email_sent: {e}")
#
# Migration: Create payment_methods, user_subscriptions, payment_transactions tables
# for table_name, create_sql in [
# ('payment_methods', f'''
# CREATE TABLE payment_methods (
# id INTEGER PRIMARY KEY {auto_increment},
# user_id INTEGER NOT NULL,
# type VARCHAR(50) NOT NULL,
# identifier VARCHAR(255) NOT NULL,
# is_default {boolean_type} DEFAULT 0,
# is_active {boolean_type} DEFAULT 1,
# metadata TEXT,
# created_at TIMESTAMP DEFAULT {timestamp_default},
# updated_at TIMESTAMP DEFAULT {timestamp_default},
# FOREIGN KEY (user_id) REFERENCES users(id)
# )
# '''),
# ('admin_settings', f'''
# CREATE TABLE admin_settings (
# id INTEGER PRIMARY KEY {auto_increment},
# setting_key VARCHAR(255) UNIQUE NOT NULL,
# setting_value TEXT,
# updated_at TIMESTAMP DEFAULT {timestamp_default}
# )
# '''),
# ('user_subscriptions', f'''
# CREATE TABLE user_subscriptions (
# id INTEGER PRIMARY KEY {auto_increment},
# user_id INTEGER NOT NULL,
# tier_id INTEGER NOT NULL,
# status VARCHAR(50) DEFAULT 'active',
# start_date TIMESTAMP DEFAULT {timestamp_default},
# end_date TIMESTAMP NULL,
# next_billing_date TIMESTAMP NULL,
# trial_end_date TIMESTAMP NULL,
# payment_method_id INTEGER,
# auto_renew {boolean_type} DEFAULT 1,
# created_at TIMESTAMP DEFAULT {timestamp_default},
# updated_at TIMESTAMP DEFAULT {timestamp_default},
# FOREIGN KEY (user_id) REFERENCES users(id),
# FOREIGN KEY (tier_id) REFERENCES account_tiers(id),
# FOREIGN KEY (payment_method_id) REFERENCES payment_methods(id),
# UNIQUE(user_id, tier_id)
# )
# '''),
# ('payment_transactions', f'''
# CREATE TABLE payment_transactions (
# id INTEGER PRIMARY KEY {auto_increment},
# user_id INTEGER NOT NULL,
# tier_id INTEGER,
# subscription_id INTEGER,
# payment_method_id INTEGER,
# amount DECIMAL(10,2) NOT NULL,
# currency VARCHAR(10) DEFAULT 'USD',
# status VARCHAR(50) NOT NULL,
# transaction_type VARCHAR(50) NOT NULL,
# external_transaction_id VARCHAR(255),
# metadata TEXT,
# created_at TIMESTAMP DEFAULT {timestamp_default},
# completed_at TIMESTAMP NULL,
# FOREIGN KEY (user_id) REFERENCES users(id),
# FOREIGN KEY (tier_id) REFERENCES account_tiers(id),
# FOREIGN KEY (subscription_id) REFERENCES user_subscriptions(id),
# FOREIGN KEY (payment_method_id) REFERENCES payment_methods(id)
# )
# ''')
# ]:
# try:
# if self.db_type == 'sqlite':
# cursor.execute(f"PRAGMA table_info({table_name})")
# if not cursor.fetchall():
# cursor.execute(create_sql)
# logger.info(f"✅ Migration: Created missing {table_name} table")
# else:
# cursor.execute(f"""
# SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES
# WHERE TABLE_NAME = '{table_name}'
# """)
# if not cursor.fetchone():
# cursor.execute(create_sql)
# logger.info(f"✅ Migration: Created missing {table_name} table")
# except Exception as e:
# logger.warning(f"Migration check for {table_name} table: {e}")
#
# conn.commit()
# logger.info("✅ All database migrations completed")
#
# else:
# CACHE DATABASE GETS MINIMAL TABLES ONLY
# cursor.execute(f'''
# CREATE TABLE IF NOT EXISTS token_usage (
# id INTEGER PRIMARY KEY {auto_increment},
# user_id INTEGER,
# provider_id VARCHAR(255) NOT NULL,
# model_name VARCHAR(255) NOT NULL,
# tokens_used INTEGER NOT NULL,
# timestamp TIMESTAMP DEFAULT {timestamp_default}
# )
# ''')
#
# cursor.execute(f'''
# CREATE TABLE IF NOT EXISTS context_dimensions (
# id INTEGER PRIMARY KEY {auto_increment},
# provider_id VARCHAR(255) NOT NULL,
# model_name VARCHAR(255) NOT NULL,
# context_size INTEGER,
# condense_context INTEGER,
# condense_method TEXT,
# effective_context INTEGER DEFAULT 0,
# last_updated TIMESTAMP DEFAULT {timestamp_default},
# UNIQUE(provider_id, model_name)
# )
# ''')
#
# logger.info("⚠️ CACHE DATABASE: Only minimal cache tables created - NO USER TABLES")
# Run configuration database migrations if this is a CONFIG database
if self.database_type == DatabaseRegistry.TYPE_CONFIG:
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):
"""Create all permanent configuration tables (CONFIG DB ONLY) - UNUSED METHOD"""
# Migration code moved to _initialize_database method
pass # Method disabled
def DatabaseManager__create_cache_tables(self, cursor, auto_increment, timestamp_default, boolean_type): def DatabaseManager__create_cache_tables(self, cursor, auto_increment, timestamp_default, boolean_type):
"""Create only temporary cache tables (CACHE DB ONLY)""" """Create only temporary cache tables (CACHE DB ONLY)"""
...@@ -4215,7 +3667,6 @@ def DatabaseManager__run_config_migrations(self, cursor, auto_increment, timesta ...@@ -4215,7 +3667,6 @@ def DatabaseManager__run_config_migrations(self, cursor, auto_increment, timesta
updated_at TIMESTAMP DEFAULT {timestamp_default} updated_at TIMESTAMP DEFAULT {timestamp_default}
) )
''') ''')
conn.commit()
logger.info("✅ Migration: Created missing account_tiers table") logger.info("✅ Migration: Created missing account_tiers table")
else: else:
cursor.execute(""" cursor.execute("""
...@@ -4244,28 +3695,40 @@ def DatabaseManager__run_config_migrations(self, cursor, auto_increment, timesta ...@@ -4244,28 +3695,40 @@ def DatabaseManager__run_config_migrations(self, cursor, auto_increment, timesta
updated_at TIMESTAMP DEFAULT {timestamp_default} updated_at TIMESTAMP DEFAULT {timestamp_default}
) )
''') ''')
conn.commit()
logger.info("✅ Migration: Created missing account_tiers table") logger.info("✅ Migration: Created missing account_tiers table")
except Exception as e: except Exception as e:
logger.warning(f"Migration check for account_tiers table: {e}") logger.warning(f"Migration check for account_tiers table: {e}")
# Migration: Add missing columns to account_tiers # Migration: Add missing columns to account_tiers
try: try:
tier_columns = [
('max_requests_per_day', 'INTEGER DEFAULT -1'),
('max_requests_per_month', 'INTEGER DEFAULT -1'),
('max_providers', 'INTEGER DEFAULT -1'),
('max_rotations', 'INTEGER DEFAULT -1'),
('max_autoselections', 'INTEGER DEFAULT -1'),
('max_rotation_models', 'INTEGER DEFAULT -1'),
('max_autoselection_models', 'INTEGER DEFAULT -1'),
('is_default', f'{boolean_type} DEFAULT 0'),
('is_active', f'{boolean_type} DEFAULT 1'),
('is_visible', f'{boolean_type} DEFAULT 1')
]
if self.db_type == 'sqlite': if self.db_type == 'sqlite':
cursor.execute("PRAGMA table_info(account_tiers)") cursor.execute("PRAGMA table_info(account_tiers)")
existing_columns = [row[1] for row in cursor.fetchall()] existing_columns = [row[1] for row in cursor.fetchall()]
tier_columns = [ col_count = 0
('max_requests_per_day', 'INTEGER DEFAULT -1'), for col_name, col_def in tier_columns:
('max_requests_per_month', 'INTEGER DEFAULT -1'), if col_name not in existing_columns:
('max_providers', 'INTEGER DEFAULT -1'), cursor.execute(f'ALTER TABLE account_tiers ADD COLUMN {col_name} {col_def}')
('max_rotations', 'INTEGER DEFAULT -1'), col_count += 1
('max_autoselections', 'INTEGER DEFAULT -1'), if col_count > 0:
('max_rotation_models', 'INTEGER DEFAULT -1'), logger.info(f"✅ Migration: Added {col_count} missing columns to account_tiers")
('max_autoselection_models', 'INTEGER DEFAULT -1'), else:
('is_default', f'{boolean_type} DEFAULT 0'), cursor.execute("""
('is_active', f'{boolean_type} DEFAULT 1'), SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS
('is_visible', f'{boolean_type} DEFAULT 1') WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'account_tiers'
] """)
existing_columns = {row[0] for row in cursor.fetchall()}
col_count = 0 col_count = 0
for col_name, col_def in tier_columns: for col_name, col_def in tier_columns:
if col_name not in existing_columns: if col_name not in existing_columns:
...@@ -4298,54 +3761,43 @@ def DatabaseManager__run_config_migrations(self, cursor, auto_increment, timesta ...@@ -4298,54 +3761,43 @@ def DatabaseManager__run_config_migrations(self, cursor, auto_increment, timesta
# Migration: Add all missing columns to users table # Migration: Add all missing columns to users table
try: try:
required_columns = [
('display_name', 'VARCHAR(255)'),
('role', "VARCHAR(50) DEFAULT 'user'"),
('created_by', 'VARCHAR(255)'),
('last_login', 'TIMESTAMP NULL'),
('is_active', f'{boolean_type} DEFAULT 1'),
('email_verified', f'{boolean_type} DEFAULT 0'),
('verification_token', 'VARCHAR(255)'),
('verification_token_expires', 'TIMESTAMP NULL'),
('last_verification_email_sent', 'TIMESTAMP NULL'),
('tier_id', 'INTEGER DEFAULT 1'),
('subscription_expires', 'TIMESTAMP NULL'),
('stripe_customer_id', 'VARCHAR(100)'),
('reset_password_token', 'VARCHAR(255)'),
('reset_password_token_expires', 'TIMESTAMP NULL'),
('profile_pic', 'TEXT'),
]
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()]
for col_name, col_def in required_columns:
required_columns = [
('username', 'VARCHAR(255) UNIQUE', 'email'),
('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'),
('profile_pic', 'TEXT', 'reset_password_token_expires')
]
for col_name, col_def, after_col in required_columns:
if col_name not in columns: if col_name not in columns:
cursor.execute(f'ALTER TABLE users ADD COLUMN {col_name} {col_def}') cursor.execute(f'ALTER TABLE users ADD COLUMN {col_name} {col_def}')
logger.info(f"✅ Migration: Added {col_name} column to users") 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:
# MySQL: check and add any missing columns cursor.execute("""
for col_name, col_def, _ in required_columns: SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS
try: WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'users'
cursor.execute(""" """)
SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS existing = {row[0] for row in cursor.fetchall()}
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'users' AND COLUMN_NAME = %s for col_name, col_def in required_columns:
""", (col_name,)) if col_name not in existing:
if not cursor.fetchone(): try:
cursor.execute(f'ALTER TABLE users ADD COLUMN {col_name} {col_def}') cursor.execute(f'ALTER TABLE users ADD COLUMN {col_name} {col_def}')
logger.info(f"✅ Migration: Added {col_name} column to users") logger.info(f"✅ Migration: Added {col_name} column to users")
except Exception as col_e: except Exception as col_e:
logger.warning(f"Migration check for users.{col_name}: {col_e}") logger.warning(f"Migration check for users.{col_name}: {col_e}")
except Exception as e: except Exception as e:
logger.warning(f"Migration check for users table columns: {e}") logger.warning(f"Migration check for users table columns: {e}")
...@@ -4438,6 +3890,149 @@ def DatabaseManager__run_config_migrations(self, cursor, auto_increment, timesta ...@@ -4438,6 +3890,149 @@ def DatabaseManager__run_config_migrations(self, cursor, auto_increment, timesta
logger.warning(f"Migration check for {table_name} table: {e}") logger.warning(f"Migration check for {table_name} table: {e}")
# Migration: Create user config tables (providers, rotations, autoselects, prompts, tokens, etc.)
for table_name, create_sql in [
('user_providers', f'''
CREATE TABLE user_providers (
id INTEGER PRIMARY KEY {auto_increment},
user_id INTEGER NOT NULL,
provider_id VARCHAR(255) NOT NULL,
config TEXT NOT NULL,
created_at TIMESTAMP DEFAULT {timestamp_default},
updated_at TIMESTAMP DEFAULT {timestamp_default},
FOREIGN KEY (user_id) REFERENCES users(id),
UNIQUE(user_id, provider_id)
)
'''),
('user_rotations', f'''
CREATE TABLE user_rotations (
id INTEGER PRIMARY KEY {auto_increment},
user_id INTEGER NOT NULL,
rotation_id VARCHAR(255) NOT NULL,
config TEXT NOT NULL,
created_at TIMESTAMP DEFAULT {timestamp_default},
updated_at TIMESTAMP DEFAULT {timestamp_default},
FOREIGN KEY (user_id) REFERENCES users(id),
UNIQUE(user_id, rotation_id)
)
'''),
('user_autoselects', f'''
CREATE TABLE user_autoselects (
id INTEGER PRIMARY KEY {auto_increment},
user_id INTEGER NOT NULL,
autoselect_id VARCHAR(255) NOT NULL,
config TEXT NOT NULL,
created_at TIMESTAMP DEFAULT {timestamp_default},
updated_at TIMESTAMP DEFAULT {timestamp_default},
FOREIGN KEY (user_id) REFERENCES users(id),
UNIQUE(user_id, autoselect_id)
)
'''),
('user_prompts', f'''
CREATE TABLE user_prompts (
id INTEGER PRIMARY KEY {auto_increment},
user_id INTEGER NOT NULL,
prompt_key VARCHAR(255) NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT {timestamp_default},
updated_at TIMESTAMP DEFAULT {timestamp_default},
FOREIGN KEY (user_id) REFERENCES users(id),
UNIQUE(user_id, prompt_key)
)
'''),
('user_api_tokens', f'''
CREATE TABLE user_api_tokens (
id INTEGER PRIMARY KEY {auto_increment},
user_id INTEGER NOT NULL,
token VARCHAR(255) UNIQUE NOT NULL,
description TEXT,
scope VARCHAR(10) DEFAULT 'api',
created_at TIMESTAMP DEFAULT {timestamp_default},
last_used TIMESTAMP NULL,
is_active {boolean_type} DEFAULT 1,
FOREIGN KEY (user_id) REFERENCES users(id)
)
'''),
('user_token_usage', f'''
CREATE TABLE user_token_usage (
id INTEGER PRIMARY KEY {auto_increment},
user_id INTEGER NOT NULL,
token_id INTEGER,
provider_id VARCHAR(255) NOT NULL,
model_name VARCHAR(255) NOT NULL,
tokens_used INTEGER NOT NULL,
timestamp TIMESTAMP DEFAULT {timestamp_default},
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (token_id) REFERENCES user_api_tokens(id)
)
'''),
('user_auth_files', f'''
CREATE TABLE user_auth_files (
id INTEGER PRIMARY KEY {auto_increment},
user_id INTEGER NOT NULL,
provider_id VARCHAR(255) NOT NULL,
file_type VARCHAR(50) NOT NULL,
original_filename VARCHAR(255) NOT NULL,
stored_filename VARCHAR(255) NOT NULL,
file_path TEXT NOT NULL,
file_size INTEGER,
mime_type VARCHAR(100),
created_at TIMESTAMP DEFAULT {timestamp_default},
updated_at TIMESTAMP DEFAULT {timestamp_default},
FOREIGN KEY (user_id) REFERENCES users(id),
UNIQUE(user_id, provider_id, file_type)
)
'''),
('user_oauth2_credentials', f'''
CREATE TABLE user_oauth2_credentials (
id INTEGER PRIMARY KEY {auto_increment},
user_id INTEGER NOT NULL,
provider_id VARCHAR(255) NOT NULL,
auth_type VARCHAR(50) NOT NULL,
credentials TEXT NOT NULL,
created_at TIMESTAMP DEFAULT {timestamp_default},
updated_at TIMESTAMP DEFAULT {timestamp_default},
FOREIGN KEY (user_id) REFERENCES users(id),
UNIQUE(user_id, provider_id, auth_type)
)
'''),
]:
try:
if self.db_type == 'sqlite':
cursor.execute(f"PRAGMA table_info({table_name})")
if not cursor.fetchall():
cursor.execute(create_sql)
logger.info(f"✅ Migration: Created {table_name} table")
else:
cursor.execute("""
SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s
""", (table_name,))
if not cursor.fetchone():
cursor.execute(create_sql)
logger.info(f"✅ Migration: Created {table_name} table")
except Exception as e:
logger.warning(f"Migration check for {table_name} table: {e}")
# Migration: Add scope column to user_api_tokens if missing
try:
if self.db_type == 'sqlite':
cursor.execute("PRAGMA table_info(user_api_tokens)")
columns = [row[1] for row in cursor.fetchall()]
if 'scope' not in columns and columns:
cursor.execute("ALTER TABLE user_api_tokens ADD COLUMN scope VARCHAR(10) DEFAULT 'api'")
logger.info("✅ Migration: Added scope column to user_api_tokens")
else:
cursor.execute("""
SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'user_api_tokens' AND COLUMN_NAME = 'scope'
""")
if not cursor.fetchone():
cursor.execute("ALTER TABLE user_api_tokens ADD COLUMN scope VARCHAR(10) DEFAULT 'api'")
logger.info("✅ Migration: Added scope column to user_api_tokens")
except Exception as e:
logger.warning(f"Migration check for user_api_tokens.scope: {e}")
logger.info("✅ All database migrations completed") logger.info("✅ All database migrations completed")
# Patch the methods # Patch the methods
......
...@@ -1504,6 +1504,20 @@ async def api_token_authorization_middleware(request: Request, call_next): ...@@ -1504,6 +1504,20 @@ async def api_token_authorization_middleware(request: Request, call_next):
"requested_user": target_username "requested_user": target_username
} }
) )
# Enforce token scope
token_scope = getattr(request.state, 'token_scope', 'both')
is_mcp_path = path.startswith("/mcp/u/") or path.startswith("/mcp/v1/u/")
if is_mcp_path and token_scope == 'api':
return JSONResponse(
status_code=403,
content={"error": "This token does not have MCP access. Create a token with 'mcp' or 'both' scope."}
)
if not is_mcp_path and token_scope == 'mcp':
return JSONResponse(
status_code=403,
content={"error": "This token does not have API access. Create a token with 'api' or 'both' scope."}
)
# --- GLOBAL ENDPOINTS (all other API paths) --- # --- GLOBAL ENDPOINTS (all other API paths) ---
else: else:
...@@ -1557,6 +1571,7 @@ async def auth_middleware(request: Request, call_next): ...@@ -1557,6 +1571,7 @@ async def auth_middleware(request: Request, call_next):
request.state.user_id = None request.state.user_id = None
request.state.token_id = None request.state.token_id = None
request.state.is_global_token = True request.state.is_global_token = True
request.state.token_scope = 'api' # global tokens are API-scope by default
request.state.is_admin = True # Global tokens have admin access request.state.is_admin = True # Global tokens have admin access
else: else:
# Check user API tokens # Check user API tokens
...@@ -1568,6 +1583,7 @@ async def auth_middleware(request: Request, call_next): ...@@ -1568,6 +1583,7 @@ async def auth_middleware(request: Request, call_next):
request.state.user_id = user_auth['user_id'] request.state.user_id = user_auth['user_id']
request.state.token_id = user_auth['token_id'] request.state.token_id = user_auth['token_id']
request.state.is_global_token = False request.state.is_global_token = False
request.state.token_scope = user_auth.get('scope', 'api')
# Store user role - admin users get full access # Store user role - admin users get full access
request.state.is_admin = (user_auth.get('role') == 'admin') request.state.is_admin = (user_auth.get('role') == 'admin')
else: else:
...@@ -1580,6 +1596,7 @@ async def auth_middleware(request: Request, call_next): ...@@ -1580,6 +1596,7 @@ async def auth_middleware(request: Request, call_next):
request.state.user_id = None request.state.user_id = None
request.state.token_id = None request.state.token_id = None
request.state.is_global_token = False request.state.is_global_token = False
request.state.token_scope = 'both'
# Check for unverified email for logged in dashboard users # Check for unverified email for logged in dashboard users
# Only enforce email verification if: # Only enforce email verification if:
...@@ -6771,7 +6788,7 @@ async def dashboard_user_tokens(request: Request): ...@@ -6771,7 +6788,7 @@ async def dashboard_user_tokens(request: Request):
) )
@app.post("/dashboard/user/tokens") @app.post("/dashboard/user/tokens")
async def dashboard_user_tokens_create(request: Request, description: str = Form("")): async def dashboard_user_tokens_create(request: Request, description: str = Form(""), scope: str = Form("api")):
"""Create a new user API token""" """Create a new user API token"""
auth_check = require_dashboard_auth(request) auth_check = require_dashboard_auth(request)
if auth_check: if auth_check:
...@@ -6781,6 +6798,9 @@ async def dashboard_user_tokens_create(request: Request, description: str = Form ...@@ -6781,6 +6798,9 @@ async def dashboard_user_tokens_create(request: Request, description: str = Form
if not user_id: if not user_id:
return JSONResponse(status_code=401, content={"error": "Not authenticated"}) return JSONResponse(status_code=401, content={"error": "Not authenticated"})
if scope not in ('api', 'mcp', 'both'):
scope = 'api'
import secrets import secrets
db = DatabaseRegistry.get_config_database() db = DatabaseRegistry.get_config_database()
...@@ -6789,11 +6809,12 @@ async def dashboard_user_tokens_create(request: Request, description: str = Form ...@@ -6789,11 +6809,12 @@ async def dashboard_user_tokens_create(request: Request, description: str = Form
token = secrets.token_urlsafe(32) token = secrets.token_urlsafe(32)
try: try:
token_id = db.create_user_api_token(user_id, token, description.strip() or None) token_id = db.create_user_api_token(user_id, token, description.strip() or None, scope)
return JSONResponse({ return JSONResponse({
"message": "Token created successfully", "message": "Token created successfully",
"token": token, "token": token,
"token_id": token_id "token_id": token_id,
"scope": scope
}) })
except Exception as e: except Exception as e:
return JSONResponse(status_code=500, content={"error": str(e)}) return JSONResponse(status_code=500, content={"error": str(e)})
...@@ -8320,6 +8341,29 @@ async def dashboard_wallet_transactions(request: Request, limit: int = 50, offse ...@@ -8320,6 +8341,29 @@ async def dashboard_wallet_transactions(request: Request, limit: int = 50, offse
return JSONResponse({"error": "Failed to load transactions"}, status_code=500) return JSONResponse({"error": "Failed to load transactions"}, status_code=500)
@app.put("/dashboard/wallet/auto-topup")
async def dashboard_wallet_auto_topup(request: Request):
"""Session-authenticated auto-topup configuration (used by the wallet dashboard page)."""
auth_check = require_dashboard_auth(request)
if auth_check:
from fastapi.responses import JSONResponse
return JSONResponse({"error": "Unauthorized"}, status_code=401)
user_id = request.session.get('user_id')
try:
body = await request.json()
from aisbf.payments.wallet.manager import WalletManager
db = DatabaseRegistry.get_config_database()
wallet_manager = WalletManager(db)
result = await wallet_manager.configure_auto_topup(user_id, body)
from fastapi.responses import JSONResponse
return JSONResponse(result)
except Exception as e:
logger.error(f"Failed to configure auto-topup: {e}")
from fastapi.responses import JSONResponse
return JSONResponse({"error": "Failed to save settings"}, status_code=500)
@app.get("/dashboard/billing") @app.get("/dashboard/billing")
async def dashboard_billing(request: Request): async def dashboard_billing(request: Request):
"""User payment transaction history page""" """User payment transaction history page"""
......
...@@ -276,6 +276,22 @@ ...@@ -276,6 +276,22 @@
<label>Description <span style="color:#555;">(optional)</span></label> <label>Description <span style="color:#555;">(optional)</span></label>
<input type="text" id="tokenDescription" placeholder="e.g. My app, Home server …"> <input type="text" id="tokenDescription" placeholder="e.g. My app, Home server …">
</div> </div>
<div class="form-group">
<label>Scope</label>
<div style="display:flex; gap:.75rem; flex-wrap:wrap; margin-top:.3rem;">
<label style="display:flex; align-items:center; gap:.4rem; cursor:pointer; color:#c0c0c0; font-size:.88rem;">
<input type="radio" name="tokenScope" value="api" checked> API only
<span style="color:#555; font-size:.78rem;">(proxy requests)</span>
</label>
<label style="display:flex; align-items:center; gap:.4rem; cursor:pointer; color:#c0c0c0; font-size:.88rem;">
<input type="radio" name="tokenScope" value="mcp"> MCP only
<span style="color:#555; font-size:.78rem;">(agent tools)</span>
</label>
<label style="display:flex; align-items:center; gap:.4rem; cursor:pointer; color:#c0c0c0; font-size:.88rem;">
<input type="radio" name="tokenScope" value="both"> Both
</label>
</div>
</div>
<div class="form-actions"> <div class="form-actions">
<button class="btn btn-primary btn-sm" onclick="submitCreateToken()">Create</button> <button class="btn btn-primary btn-sm" onclick="submitCreateToken()">Create</button>
<button class="btn btn-secondary btn-sm" onclick="toggleCreateForm()">Cancel</button> <button class="btn btn-secondary btn-sm" onclick="toggleCreateForm()">Cancel</button>
...@@ -309,17 +325,27 @@ ...@@ -309,17 +325,27 @@
<p style="color:#a0a0a0; font-size:.88rem; margin-bottom:.75rem;">Add the token to every request in the <code style="background:#0f3460; padding:.1rem .35rem; border-radius:3px;">Authorization</code> header:</p> <p style="color:#a0a0a0; font-size:.88rem; margin-bottom:.75rem;">Add the token to every request in the <code style="background:#0f3460; padding:.1rem .35rem; border-radius:3px;">Authorization</code> header:</p>
<div class="code-block">Authorization: Bearer YOUR_API_TOKEN</div> <div class="code-block">Authorization: Bearer YOUR_API_TOKEN</div>
<p style="color:#a0a0a0; font-size:.88rem; margin-top:1.25rem; margin-bottom:.5rem;">Token scopes:</p>
<table class="ep-table">
<thead><tr><th>Scope</th><th>Access</th></tr></thead>
<tbody>
<tr><td><code style="color:#60a5fa;">api</code></td><td>Proxy API endpoints only (<code>/api/u/…</code>)</td></tr>
<tr><td><code style="color:#a78bfa;">mcp</code></td><td>MCP tool endpoints only (<code>/mcp/u/…</code>)</td></tr>
<tr><td><code style="color:#4ade80;">both</code></td><td>Both API and MCP endpoints</td></tr>
</tbody>
</table>
<p style="color:#a0a0a0; font-size:.88rem; margin-top:1.25rem; margin-bottom:.5rem;">Available endpoints:</p> <p style="color:#a0a0a0; font-size:.88rem; margin-top:1.25rem; margin-bottom:.5rem;">Available endpoints:</p>
<table class="ep-table"> <table class="ep-table">
<thead><tr><th>Method</th><th>Endpoint</th><th>Description</th></tr></thead> <thead><tr><th>Method</th><th>Endpoint</th><th>Scope</th><th>Description</th></tr></thead>
<tbody> <tbody>
<tr><td><span class="method-badge method-GET">GET</span></td> <td><code>/api/u/{{ session.username }}/models</code></td> <td>List your models</td></tr> <tr><td><span class="method-badge method-GET">GET</span></td> <td><code>/api/u/{{ session.username }}/models</code></td> <td><code style="color:#60a5fa;">api</code></td><td>List your models</td></tr>
<tr><td><span class="method-badge method-GET">GET</span></td> <td><code>/api/u/{{ session.username }}/providers</code></td> <td>List your providers</td></tr> <tr><td><span class="method-badge method-GET">GET</span></td> <td><code>/api/u/{{ session.username }}/providers</code></td> <td><code style="color:#60a5fa;">api</code></td><td>List your providers</td></tr>
<tr><td><span class="method-badge method-GET">GET</span></td> <td><code>/api/u/{{ session.username }}/rotations</code></td> <td>List your rotations</td></tr> <tr><td><span class="method-badge method-GET">GET</span></td> <td><code>/api/u/{{ session.username }}/rotations</code></td> <td><code style="color:#60a5fa;">api</code></td><td>List your rotations</td></tr>
<tr><td><span class="method-badge method-GET">GET</span></td> <td><code>/api/u/{{ session.username }}/autoselects</code></td> <td>List your autoselects</td></tr> <tr><td><span class="method-badge method-GET">GET</span></td> <td><code>/api/u/{{ session.username }}/autoselects</code></td> <td><code style="color:#60a5fa;">api</code></td><td>List your autoselects</td></tr>
<tr><td><span class="method-badge method-POST">POST</span></td><td><code>/api/u/{{ session.username }}/chat/completions</code></td><td>Chat using your configs</td></tr> <tr><td><span class="method-badge method-POST">POST</span></td><td><code>/api/u/{{ session.username }}/chat/completions</code></td><td><code style="color:#60a5fa;">api</code></td><td>Chat using your configs</td></tr>
<tr><td><span class="method-badge method-GET">GET</span></td> <td><code>/mcp/u/{{ session.username }}/tools</code></td> <td>List MCP tools</td></tr> <tr><td><span class="method-badge method-GET">GET</span></td> <td><code>/mcp/u/{{ session.username }}/tools</code></td> <td><code style="color:#a78bfa;">mcp</code></td><td>List MCP tools</td></tr>
<tr><td><span class="method-badge method-POST">POST</span></td><td><code>/mcp/u/{{ session.username }}/tools/call</code></td> <td>Call MCP tools</td></tr> <tr><td><span class="method-badge method-POST">POST</span></td><td><code>/mcp/u/{{ session.username }}/tools/call</code></td> <td><code style="color:#a78bfa;">mcp</code></td><td>Call MCP tools</td></tr>
</tbody> </tbody>
</table> </table>
...@@ -353,7 +379,10 @@ function renderTokens() { ...@@ -353,7 +379,10 @@ function renderTokens() {
return; return;
} }
list.innerHTML = tokens.map(t => ` list.innerHTML = tokens.map(t => {
const scopeLabel = {api: 'API', mcp: 'MCP', both: 'API+MCP'}[t.scope || 'api'] || 'API';
const scopeColor = {api: '#60a5fa', mcp: '#a78bfa', both: '#4ade80'}[t.scope || 'api'] || '#60a5fa';
return `
<div class="token-card" id="token-${t.id}"> <div class="token-card" id="token-${t.id}">
<div class="token-card-top"> <div class="token-card-top">
<div> <div>
...@@ -361,6 +390,7 @@ function renderTokens() { ...@@ -361,6 +390,7 @@ function renderTokens() {
<div class="token-meta"> <div class="token-meta">
<span><i class="fas fa-calendar-plus"></i> Created ${formatDate(t.created_at)}</span> <span><i class="fas fa-calendar-plus"></i> Created ${formatDate(t.created_at)}</span>
${t.last_used ? `<span><i class="fas fa-clock"></i> Last used ${formatDate(t.last_used)}</span>` : ''} ${t.last_used ? `<span><i class="fas fa-clock"></i> Last used ${formatDate(t.last_used)}</span>` : ''}
<span style="color:${scopeColor};"><i class="fas fa-shield-halved"></i> ${scopeLabel}</span>
</div> </div>
</div> </div>
<div class="token-actions"> <div class="token-actions">
...@@ -377,7 +407,7 @@ function renderTokens() { ...@@ -377,7 +407,7 @@ function renderTokens() {
<button class="copy-btn" onclick="copyPartialToken(this, ${JSON.stringify(t.token)})">Copy full</button> <button class="copy-btn" onclick="copyPartialToken(this, ${JSON.stringify(t.token)})">Copy full</button>
</div> </div>
</div> </div>
`).join(''); `}).join('');
} }
function escHtml(s) { function escHtml(s) {
...@@ -404,8 +434,10 @@ function toggleCreateForm() { ...@@ -404,8 +434,10 @@ function toggleCreateForm() {
function submitCreateToken() { function submitCreateToken() {
const desc = document.getElementById('tokenDescription').value.trim(); const desc = document.getElementById('tokenDescription').value.trim();
const scope = document.querySelector('input[name="tokenScope"]:checked')?.value || 'api';
const formData = new FormData(); const formData = new FormData();
if (desc) formData.append('description', desc); if (desc) formData.append('description', desc);
formData.append('scope', scope);
fetch('{{ url_for(request, "/dashboard/user/tokens") }}', { fetch('{{ url_for(request, "/dashboard/user/tokens") }}', {
method: 'POST', method: 'POST',
...@@ -425,7 +457,8 @@ function submitCreateToken() { ...@@ -425,7 +457,8 @@ function submitCreateToken() {
description: desc || null, description: desc || null,
created_at: new Date().toISOString(), created_at: new Date().toISOString(),
last_used: null, last_used: null,
is_active: true is_active: true,
scope: data.scope || scope
}); });
renderTokens(); renderTokens();
}) })
......
...@@ -350,7 +350,7 @@ document.addEventListener('DOMContentLoaded', function () { ...@@ -350,7 +350,7 @@ document.addEventListener('DOMContentLoaded', function () {
threshold_amount: parseFloat(document.getElementById('auto-topup-threshold').value) || null, threshold_amount: parseFloat(document.getElementById('auto-topup-threshold').value) || null,
payment_method: 'stripe' payment_method: 'stripe'
}; };
fetch('/api/wallet/auto-topup', { fetch('/dashboard/wallet/auto-topup', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload) body: JSON.stringify(payload)
......
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