Commit ceafa183 authored by Your Name's avatar Your Name

Ties system and bug fixes

parent f4c1b651
# AISBF Database Migration Guide
## Overview
AISBF uses two separate SQLite databases with distinct purposes:
1. **`aisbf.db`** - Configuration and persistent data
2. **`cache.db`** - Temporary caching only
## Database Separation
### aisbf.db (Configuration Database)
This database contains all configuration and persistent data:
**User Management:**
- `users` - User accounts and authentication
- `user_api_tokens` - API tokens for users
- `user_providers` - User-specific provider configurations
- `user_rotations` - User-specific rotation configurations
- `user_autoselects` - User-specific autoselect configurations
- `user_prompts` - User-specific prompt overrides
- `user_auth_files` - User authentication file metadata
- `user_oauth2_credentials` - OAuth2 credentials per user/provider
**Billing & Subscriptions:**
- `account_tiers` - Subscription tier definitions
- `payment_methods` - User payment methods
- `user_subscriptions` - Active subscriptions
- `payment_transactions` - Payment history
**Analytics & Tracking:**
- `context_dimensions` - Context usage tracking
- `token_usage` - Token usage for rate limiting
- `user_token_usage` - User-specific token usage
- `model_embeddings` - Cached model embeddings
### cache.db (Cache Database)
This database contains ONLY temporary caching data:
- `cache` - General purpose cache
- `response_cache` - AI response caching
## Migration Issue
In some installations, configuration tables (especially `users`) were incorrectly created in `cache.db` instead of `aisbf.db`. This causes issues because:
1. Configuration data should persist across cache clears
2. The application expects configuration in `aisbf.db`
3. Cache database should be safe to delete without losing data
## Migration Process
### Step 1: Check Current State
First, verify which database contains your data:
```bash
# Check tables in cache.db
sqlite3 ~/.aisbf/cache.db ".tables"
# Check tables in aisbf.db
sqlite3 ~/.aisbf/aisbf.db ".tables"
```
If you see configuration tables (like `users`, `user_providers`, etc.) in `cache.db`, you need to migrate.
### Step 2: Dry Run
Test the migration without making changes:
```bash
python migrate_cache_to_aisbf.py --dry-run
```
This will show you:
- Which tables will be migrated
- How many rows will be copied
- Any potential issues
### Step 3: Perform Migration
Run the actual migration:
```bash
python migrate_cache_to_aisbf.py
```
The script will:
1. Create backups of both databases
2. Copy configuration tables from `cache.db` to `aisbf.db`
3. Preserve all existing data
4. Show a summary of migrated data
**Backup files are created automatically:**
- `~/.aisbf/cache_backup_YYYYMMDD_HHMMSS.db`
- `~/.aisbf/aisbf_backup_YYYYMMDD_HHMMSS.db`
### Step 4: Verify Migration
After migration, verify the data:
```bash
# Check users table in aisbf.db
sqlite3 ~/.aisbf/aisbf.db "SELECT COUNT(*) FROM users;"
# Check your user exists
sqlite3 ~/.aisbf/aisbf.db "SELECT username, role FROM users;"
```
### Step 5: Test Application
Start AISBF and verify:
1. You can log in with your existing credentials
2. All providers and rotations are available
3. User-specific configurations are preserved
```bash
# Start AISBF
aisbf
# Or if running from source
python main.py
```
### Step 6: Cleanup (Optional)
After confirming everything works, clean up `cache.db`:
```bash
python migrate_cache_to_aisbf.py --cleanup
```
This removes configuration tables from `cache.db`, leaving only cache tables.
## Advanced Options
### Force Overwrite
If destination tables already have data and you want to overwrite:
```bash
python migrate_cache_to_aisbf.py --force
```
### Custom Database Paths
If your databases are in non-standard locations:
```bash
python migrate_cache_to_aisbf.py \
--cache-db /path/to/cache.db \
--aisbf-db /path/to/aisbf.db
```
## Troubleshooting
### Issue: "Table already has rows in destination"
**Solution:** Use `--force` to overwrite, or manually inspect both databases to determine which has the correct data.
### Issue: Migration fails with "database is locked"
**Solution:** Stop AISBF before running migration:
```bash
# Stop AISBF
aisbf stop
# Run migration
python migrate_cache_to_aisbf.py
# Start AISBF
aisbf
```
### Issue: Lost data after migration
**Solution:** Restore from backup:
```bash
# Find your backup
ls -lt ~/.aisbf/*_backup_*.db
# Restore cache.db
cp ~/.aisbf/cache_backup_YYYYMMDD_HHMMSS.db ~/.aisbf/cache.db
# Restore aisbf.db
cp ~/.aisbf/aisbf_backup_YYYYMMDD_HHMMSS.db ~/.aisbf/aisbf.db
```
## Prevention
The code has been updated to ensure proper database separation:
1. **`database.py`** - All methods use `aisbf.db` for configuration
2. **`cache.py`** - All methods use `cache.db` for caching only
3. **Initialization** - Databases are created with correct table separation
After upgrading to the fixed version, new installations will automatically use the correct database structure.
## For Developers
### Database Initialization
```python
from aisbf.database import initialize_database, get_database
# Initialize configuration database (aisbf.db)
initialize_database()
# Get database manager
db = get_database()
# All operations use aisbf.db
user = db.authenticate_user(username, password_hash)
```
### Cache Operations
```python
from aisbf.cache import get_cache_manager
# Initialize cache (cache.db)
cache = get_cache_manager()
# All operations use cache.db
cache.set('key', 'value', ttl=600)
value = cache.get('key')
```
### Adding New Tables
**Configuration tables** (add to `database.py`):
```python
cursor.execute('''
CREATE TABLE IF NOT EXISTS my_config_table (
id INTEGER PRIMARY KEY AUTOINCREMENT,
...
)
''')
```
**Cache tables** (add to `cache.py`):
```python
cursor.execute('''
CREATE TABLE IF NOT EXISTS my_cache_table (
key TEXT PRIMARY KEY,
value TEXT,
ttl REAL
)
''')
```
## Summary
- **aisbf.db** = Configuration & persistent data (users, providers, etc.)
- **cache.db** = Temporary caching only (cache, response_cache)
- **Migration script** = Moves misplaced tables from cache.db to aisbf.db
- **Backups** = Created automatically before any changes
- **Safe** = Can be run multiple times, dry-run available
For questions or issues, refer to the main DOCUMENTATION.md or open an issue on GitHub.
......@@ -48,7 +48,7 @@ class KiloOAuth2:
credentials_file: Path to credentials JSON file (default: ~/.kilo_credentials.json)
api_base: Base URL for Kilo API (default: https://api.kilo.ai)
"""
self.credentials_file = credentials_file or os.path.expanduser("~/.kilo_credentials.json")
self.credentials_file = os.path.expanduser(credentials_file) if credentials_file else os.path.expanduser("~/.kilo_credentials.json")
self.api_base = api_base or os.environ.get("KILO_API_URL", "https://api.kilo.ai")
self.credentials = None
self._load_credentials()
......@@ -73,7 +73,9 @@ class KiloOAuth2:
"""
try:
# Ensure directory exists
os.makedirs(os.path.dirname(self.credentials_file), exist_ok=True)
cred_dir = os.path.dirname(self.credentials_file)
if cred_dir: # Only create if there's a directory component
os.makedirs(cred_dir, exist_ok=True)
# Write credentials
with open(self.credentials_file, 'w') as f:
......
......@@ -219,6 +219,24 @@ class SignupConfig(BaseModel):
require_email_verification: bool = True
verification_token_expiry_hours: int = 24
class PaymentGatewayConfig(BaseModel):
"""Configuration for payment gateways"""
enabled: bool = False
public_key: Optional[str] = None
secret_key: Optional[str] = None
webhook_secret: Optional[str] = None
api_url: Optional[str] = None
wallet_address: Optional[str] = None
minimum_amount: Optional[float] = None
additional_config: Optional[Dict] = None
class CurrencyConfig(BaseModel):
"""Global currency configuration"""
code: str = "USD"
symbol: str = "$"
decimal_places: int = 2
position: str = "left"
class SMTPConfig(BaseModel):
"""Configuration for SMTP email sending"""
host: str = "localhost"
......@@ -248,6 +266,8 @@ class AISBFConfig(BaseModel):
adaptive_rate_limiting: Optional[AdaptiveRateLimitingConfig] = None
signup: Optional[SignupConfig] = None
smtp: Optional[SMTPConfig] = None
currency: Optional[CurrencyConfig] = None
payment_gateways: Optional[Dict[str, PaymentGatewayConfig]] = None
class AppConfig(BaseModel):
......@@ -728,6 +748,14 @@ class Config:
smtp_data = data.get('smtp')
if smtp_data:
data['smtp'] = SMTPConfig(**smtp_data)
# Parse currency separately if present
currency_data = data.get('currency')
if currency_data:
data['currency'] = CurrencyConfig(**currency_data)
# Parse payment gateways separately if present
payment_gateways_data = data.get('payment_gateways')
if payment_gateways_data:
data['payment_gateways'] = {k: PaymentGatewayConfig(**v) for k, v in payment_gateways_data.items()}
self.aisbf = AISBFConfig(**data)
self._loaded_files['aisbf'] = str(aisbf_path.absolute())
logger.info(f"Loaded AISBF config: classify_nsfw={self.aisbf.classify_nsfw}, classify_privacy={self.aisbf.classify_privacy}")
......
"""
Copyleft (C) 2026 Stefy Lanza <stefy@nexlab.net>
AISBF - AI Service Broker Framework || AI Should Be Free
Database module for persistent tracking of context dimensions and rate limiting.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
......@@ -70,7 +69,6 @@ class DatabaseManager:
self.db_config = db_config
self.db_type = self.db_config.get('type', 'sqlite').lower()
self.connection = None
if self.db_type == 'mysql' and not MYSQL_AVAILABLE:
raise ImportError("MySQL connector not available. Install mysql-connector-python.")
......@@ -235,56 +233,77 @@ class DatabaseManager:
''')
# Migration: Add email-related columns if they don't exist
try:
# EACH COLUMN WITH INDIVIDUAL TRY/CATCH TO AVOID FAILURE CASCADING
if self.db_type == 'sqlite':
cursor.execute("PRAGMA table_info(users)")
columns = [row[1] for row in cursor.fetchall()]
try:
if 'email' not in columns:
cursor.execute('ALTER TABLE users ADD COLUMN email VARCHAR(255) UNIQUE')
logger.info("Migration: Added email column to users table")
cursor.execute('ALTER TABLE users ADD COLUMN email VARCHAR(255)')
logger.info("✅ Migration: Added email column to users table")
cursor.execute('UPDATE users SET email = username WHERE email IS NULL')
logger.info("✅ Migration: Populated default emails for existing users")
cursor.execute('CREATE UNIQUE INDEX IF NOT EXISTS idx_users_email ON users(email)')
logger.info("✅ Migration: Added UNIQUE index on email column")
conn.commit()
except Exception as e:
logger.warning(f"Migration: email column: {e}")
try:
if 'email_verified' not in columns:
cursor.execute(f'ALTER TABLE users ADD COLUMN email_verified {boolean_type} DEFAULT 0')
logger.info("Migration: Added email_verified column to users table")
logger.info("✅ Migration: Added email_verified column to users table")
conn.commit()
except Exception as e:
logger.warning(f"Migration: email_verified column: {e}")
try:
if 'verification_token' not in columns:
cursor.execute('ALTER TABLE users ADD COLUMN verification_token VARCHAR(255)')
logger.info("Migration: Added verification_token column to users table")
logger.info("✅ Migration: Added verification_token column to users table")
conn.commit()
except Exception as e:
logger.warning(f"Migration: verification_token column: {e}")
try:
if 'verification_token_expires' not in columns:
cursor.execute('ALTER TABLE users ADD COLUMN verification_token_expires TIMESTAMP NULL')
logger.info("Migration: Added verification_token_expires column to users table")
logger.info("✅ Migration: Added verification_token_expires column to users table")
conn.commit()
except Exception as e:
logger.warning(f"Migration: verification_token_expires column: {e}")
else: # mysql
try:
cursor.execute("""
SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'users' AND COLUMN_NAME = 'email'
""")
if not cursor.fetchone():
cursor.execute('ALTER TABLE users ADD COLUMN email VARCHAR(255) UNIQUE')
logger.info("Migration: Added email column to users table")
cursor.execute("""
SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'users' AND COLUMN_NAME = 'email_verified'
""")
if not cursor.fetchone():
cursor.execute(f'ALTER TABLE users ADD COLUMN email_verified {boolean_type} DEFAULT 0')
logger.info("Migration: Added email_verified column to users table")
cursor.execute("""
SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'users' AND COLUMN_NAME = 'verification_token'
""")
if not cursor.fetchone():
cursor.execute('ALTER TABLE users ADD COLUMN verification_token VARCHAR(255)')
logger.info("Migration: Added verification_token column to users table")
cursor.execute('ALTER TABLE users ADD COLUMN email VARCHAR(255)')
cursor.execute('UPDATE users SET email = username WHERE email IS NULL')
cursor.execute('CREATE UNIQUE INDEX idx_users_email ON users(email)')
logger.info("✅ Migration: Added email column with UNIQUE constraint")
conn.commit()
except Exception as e:
logger.warning(f"Migration: email column: {e}")
cursor.execute("""
for col_name, col_def in [
('email_verified', f'{boolean_type} DEFAULT 0'),
('verification_token', 'VARCHAR(255)'),
('verification_token_expires', 'TIMESTAMP NULL')
]:
try:
cursor.execute(f"""
SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'users' AND COLUMN_NAME = 'verification_token_expires'
WHERE TABLE_NAME = 'users' AND COLUMN_NAME = '{col_name}'
""")
if not cursor.fetchone():
cursor.execute('ALTER TABLE users ADD COLUMN verification_token_expires TIMESTAMP NULL')
logger.info("Migration: Added verification_token_expires column to users table")
cursor.execute(f'ALTER TABLE users ADD COLUMN {col_name} {col_def}')
logger.info(f"✅ Migration: Added {col_name} column to users table")
conn.commit()
except Exception as e:
logger.warning(f"Migration check for users email fields: {e}")
logger.warning(f"Migration: {col_name} column: {e}")
# User-specific configuration tables for multi-user isolation
cursor.execute(f'''
......@@ -422,6 +441,213 @@ class DatabaseManager:
conn.commit()
logger.info("User auth files table initialized")
# ==============================================
# UNIVERSAL MIGRATIONS - RUN ON EVERY STARTUP
# These run for ALL databases, new and existing
# ==============================================
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")
else:
cursor.execute("""
SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_NAME = 'account_tiers'
""")
if not cursor.fetchone():
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')
]
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")
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: 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)
)
'''),
('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")
def record_context_dimension(
self,
provider_id: str,
......@@ -809,20 +1035,42 @@ class DatabaseManager:
with self._get_connection() as conn:
cursor = conn.cursor()
placeholder = '?' if self.db_type == 'sqlite' else '%s'
# First check what columns exist in users table
cursor.execute("PRAGMA table_info(users)")
columns = [col[1] for col in cursor.fetchall()]
select_fields = ['id', 'username', 'role', 'is_active']
if 'email' in columns:
select_fields.append('email')
if 'email_verified' in columns:
select_fields.append('email_verified')
cursor.execute(f'''
SELECT id, username, role, is_active
SELECT {', '.join(select_fields)}
FROM users
WHERE username = {placeholder} AND password_hash = {placeholder} AND is_active = 1
''', (username, password_hash))
row = cursor.fetchone()
if row:
return {
result = {
'id': row[0],
'username': row[1],
'role': row[2],
'is_active': row[3]
'is_active': row[3],
'email': None,
'email_verified': True
}
idx = 4
if 'email' in columns:
result['email'] = row[idx] or None
idx += 1
if 'email_verified' in columns:
result['email_verified'] = bool(row[idx]) if row[idx] is not None else True
return result
return None
def get_user_by_username(self, username: str) -> Optional[Dict]:
......@@ -1840,7 +2088,7 @@ class DatabaseManager:
(user_id, provider_id, auth_type, credentials, updated_at)
VALUES ({placeholder}, {placeholder}, {placeholder}, {placeholder}, CURRENT_TIMESTAMP)
''', (user_id, provider_id, auth_type, credentials_json))
else: # mysql
else:
cursor.execute(f'''
INSERT INTO user_oauth2_credentials
(user_id, provider_id, auth_type, credentials, updated_at)
......@@ -1957,35 +2205,1029 @@ class DatabaseManager:
conn.commit()
return cursor.rowcount
# Account Tier methods
def get_all_tiers(self) -> List[Dict]:
"""
Get all account tiers.
Returns:
List of tier dictionaries
"""
with self._get_connection() as conn:
cursor = conn.cursor()
cursor.execute('''
SELECT id, 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,
created_at, updated_at
FROM account_tiers
ORDER BY price_monthly ASC
''')
tiers = []
for row in cursor.fetchall():
tiers.append({
'id': row[0],
'name': row[1],
'description': row[2],
'price_monthly': float(row[3]),
'price_yearly': float(row[4]),
'is_default': bool(row[5]),
'is_active': bool(row[6]),
'max_requests_per_day': row[7],
'max_requests_per_month': row[8],
'max_providers': row[9],
'max_rotations': row[10],
'max_autoselections': row[11],
'max_rotation_models': row[12],
'max_autoselection_models': row[13],
'created_at': row[14],
'updated_at': row[15]
})
return tiers
# Global database manager instance
_db_manager: Optional[DatabaseManager] = None
def get_tier_by_id(self, tier_id: int) -> Optional[Dict]:
"""
Get a specific tier by ID.
Args:
tier_id: Tier ID
def get_database(db_config: Optional[Dict[str, Any]] = None) -> DatabaseManager:
Returns:
Tier dictionary or None
"""
with self._get_connection() as conn:
cursor = conn.cursor()
placeholder = '?' if self.db_type == 'sqlite' else '%s'
cursor.execute(f'''
SELECT id, 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,
created_at, updated_at
FROM account_tiers
WHERE id = {placeholder}
''', (tier_id,))
row = cursor.fetchone()
if row:
return {
'id': row[0],
'name': row[1],
'description': row[2],
'price_monthly': float(row[3]),
'price_yearly': float(row[4]),
'is_default': bool(row[5]),
'is_active': bool(row[6]),
'max_requests_per_day': row[7],
'max_requests_per_month': row[8],
'max_providers': row[9],
'max_rotations': row[10],
'max_autoselections': row[11],
'max_rotation_models': row[12],
'max_autoselection_models': row[13],
'created_at': row[14],
'updated_at': row[15]
}
return None
def create_tier(self, name: str, description: str, price_monthly: float, price_yearly: float,
max_requests_per_day: int = -1, max_requests_per_month: int = -1,
max_providers: int = -1, max_rotations: int = -1,
max_autoselections: int = -1, max_rotation_models: int = -1,
max_autoselection_models: int = -1, is_active: bool = True) -> int:
"""
Get the global database manager instance.
Create a new account tier.
Args:
db_config: Database configuration. If None, uses default.
name: Tier name
description: Tier description
price_monthly: Monthly price
price_yearly: Yearly price
max_requests_per_day: Max requests per day (-1 for unlimited)
max_requests_per_month: Max requests per month (-1 for unlimited)
max_providers: Max providers allowed (-1 for unlimited)
max_rotations: Max rotations allowed (-1 for unlimited)
max_autoselections: Max autoselections allowed (-1 for unlimited)
max_rotation_models: Max models per rotation (-1 for unlimited)
max_autoselection_models: Max models per autoselection (-1 for unlimited)
is_active: Whether tier is active
Returns:
Created tier ID
"""
with self._get_connection() as conn:
cursor = conn.cursor()
placeholder = '?' if self.db_type == 'sqlite' else '%s'
cursor.execute(f'''
INSERT INTO account_tiers
(name, description, price_monthly, price_yearly, is_active,
max_requests_per_day, max_requests_per_month, max_providers, max_rotations,
max_autoselections, max_rotation_models, max_autoselection_models)
VALUES ({placeholder}, {placeholder}, {placeholder}, {placeholder}, {placeholder},
{placeholder}, {placeholder}, {placeholder}, {placeholder},
{placeholder}, {placeholder}, {placeholder})
''', (name, description, price_monthly, price_yearly, 1 if is_active else 0,
max_requests_per_day, max_requests_per_month, max_providers, max_rotations,
max_autoselections, max_rotation_models, max_autoselection_models))
conn.commit()
return cursor.lastrowid
def update_tier(self, tier_id: int, **kwargs) -> bool:
"""
Update an existing tier.
Args:
tier_id: Tier ID
**kwargs: Fields to update
Returns:
The DatabaseManager instance
True if updated, False otherwise
"""
global _db_manager
if _db_manager is None:
_db_manager = DatabaseManager(db_config)
return _db_manager
with self._get_connection() as conn:
cursor = conn.cursor()
placeholder = '?' if self.db_type == 'sqlite' else '%s'
updates = []
params = []
allowed_fields = ['name', 'description', 'price_monthly', 'price_yearly', 'is_active',
'max_requests_per_day', 'max_requests_per_month', 'max_providers',
'max_rotations', 'max_autoselections', 'max_rotation_models',
'max_autoselection_models']
def initialize_database(db_config: Optional[Dict[str, Any]] = None):
for field in allowed_fields:
if field in kwargs:
updates.append(f"{field} = {placeholder}")
params.append(kwargs[field])
if not updates:
return False
params.append(tier_id)
query = f"UPDATE account_tiers SET {', '.join(updates)}, updated_at = CURRENT_TIMESTAMP WHERE id = {placeholder}"
cursor.execute(query, params)
conn.commit()
return cursor.rowcount > 0
def delete_tier(self, tier_id: int) -> bool:
"""
Initialize the database and clean up old records.
This should be called at application startup.
Delete a tier (cannot delete default tier).
Args:
db_config: Database configuration. If None, uses default.
tier_id: Tier ID
Returns:
True if deleted, False otherwise
"""
db = get_database(db_config)
db.cleanup_old_token_usage(days_to_keep=7)
logger.info("Database initialized and old records cleaned up")
\ No newline at end of file
with self._get_connection() as conn:
cursor = conn.cursor()
placeholder = '?' if self.db_type == 'sqlite' else '%s'
# Prevent deleting default tier
cursor.execute(f'''
DELETE FROM account_tiers
WHERE id = {placeholder} AND is_default = 0
''', (tier_id,))
conn.commit()
return cursor.rowcount > 0
def get_user_tier(self, user_id: int) -> Optional[Dict]:
"""
Get the current tier for a user.
Args:
user_id: User ID
Returns:
Tier dictionary or None
"""
with self._get_connection() as conn:
cursor = conn.cursor()
placeholder = '?' if self.db_type == 'sqlite' else '%s'
cursor.execute(f'''
SELECT t.id, t.name, t.description, t.price_monthly, t.price_yearly,
t.max_requests_per_day, t.max_requests_per_month, t.max_providers,
t.max_rotations, t.max_autoselections, t.max_rotation_models,
t.max_autoselection_models
FROM users u
JOIN account_tiers t ON u.tier_id = t.id
WHERE u.id = {placeholder}
''', (user_id,))
row = cursor.fetchone()
if row:
return {
'id': row[0],
'name': row[1],
'description': row[2],
'price_monthly': float(row[3]),
'price_yearly': float(row[4]),
'max_requests_per_day': row[5],
'max_requests_per_month': row[6],
'max_providers': row[7],
'max_rotations': row[8],
'max_autoselections': row[9],
'max_rotation_models': row[10],
'max_autoselection_models': row[11]
}
return None
def set_user_tier(self, user_id: int, tier_id: int) -> bool:
"""
Set the tier for a user.
Args:
user_id: User ID
tier_id: Tier ID
Returns:
True if updated, False otherwise
"""
with self._get_connection() as conn:
cursor = conn.cursor()
placeholder = '?' if self.db_type == 'sqlite' else '%s'
cursor.execute(f'''
UPDATE users
SET tier_id = {placeholder}
WHERE id = {placeholder}
''', (tier_id, user_id))
conn.commit()
return cursor.rowcount > 0
# Payment and Subscription methods
def get_user_payment_methods(self, user_id: int) -> List[Dict]:
"""Get all payment methods for a user."""
with self._get_connection() as conn:
cursor = conn.cursor()
placeholder = '?' if self.db_type == 'sqlite' else '%s'
cursor.execute(f'''
SELECT id, type, identifier, is_default, is_active, metadata, created_at
FROM payment_methods
WHERE user_id = {placeholder}
ORDER BY is_default DESC, created_at DESC
''', (user_id,))
methods = []
for row in cursor.fetchall():
methods.append({
'id': row[0], 'type': row[1], 'identifier': row[2],
'is_default': bool(row[3]), 'is_active': bool(row[4]),
'metadata': json.loads(row[5]) if row[5] else {},
'created_at': row[6]
})
return methods
def add_payment_method(self, user_id: int, method_type: str, identifier: str,
is_default: bool = False, metadata: Dict = None) -> int:
"""Add a payment method for a user."""
with self._get_connection() as conn:
cursor = conn.cursor()
placeholder = '?' if self.db_type == 'sqlite' else '%s'
if is_default:
cursor.execute(f'''
UPDATE payment_methods SET is_default = 0
WHERE user_id = {placeholder}
''', (user_id,))
cursor.execute(f'''
INSERT INTO payment_methods (user_id, type, identifier, is_default, metadata)
VALUES ({placeholder}, {placeholder}, {placeholder}, {placeholder}, {placeholder})
''', (user_id, method_type, identifier, 1 if is_default else 0,
json.dumps(metadata) if metadata else None))
conn.commit()
return cursor.lastrowid
def delete_payment_method(self, user_id: int, method_id: int) -> bool:
"""Delete a payment method."""
with self._get_connection() as conn:
cursor = conn.cursor()
placeholder = '?' if self.db_type == 'sqlite' else '%s'
cursor.execute(f'''
DELETE FROM payment_methods
WHERE id = {placeholder} AND user_id = {placeholder}
''', (method_id, user_id))
conn.commit()
return cursor.rowcount > 0
def get_user_subscription(self, user_id: int) -> Optional[Dict]:
"""Get current active subscription for a user."""
with self._get_connection() as conn:
cursor = conn.cursor()
placeholder = '?' if self.db_type == 'sqlite' else '%s'
cursor.execute(f'''
SELECT s.id, s.tier_id, t.name, s.status, s.start_date, s.end_date,
s.next_billing_date, s.auto_renew, t.price_monthly, t.price_yearly
FROM user_subscriptions s
JOIN account_tiers t ON s.tier_id = t.id
WHERE s.user_id = {placeholder} AND s.status = 'active'
ORDER BY s.created_at DESC LIMIT 1
''', (user_id,))
row = cursor.fetchone()
if row:
return {
'id': row[0], 'tier_id': row[1], 'tier_name': row[2],
'status': row[3], 'start_date': row[4], 'end_date': row[5],
'next_billing_date': row[6], 'auto_renew': bool(row[7]),
'price_monthly': float(row[8]), 'price_yearly': float(row[9])
}
return None
def get_user_payment_transactions(self, user_id: int, limit: int = 50) -> List[Dict]:
"""Get payment transaction history for a user."""
with self._get_connection() as conn:
cursor = conn.cursor()
placeholder = '?' if self.db_type == 'sqlite' else '%s'
cursor.execute(f'''
SELECT id, amount, currency, status, transaction_type,
external_transaction_id, created_at, completed_at
FROM payment_transactions
WHERE user_id = {placeholder}
ORDER BY created_at DESC LIMIT {placeholder}
''', (user_id, limit))
transactions = []
for row in cursor.fetchall():
transactions.append({
'id': row[0], 'amount': float(row[1]), 'currency': row[2],
'status': row[3], 'transaction_type': row[4],
'external_transaction_id': row[5],
'created_at': row[6], 'completed_at': row[7]
})
return transactions
# ============================================================
# DATABASE REGISTRY - EXPLICIT NAMED INSTANCES
# No more accidental wrong database connections!
# ============================================================
class DatabaseRegistry:
"""
Explicit registry for database connections.
This prevents accidental mixing of configuration and cache databases.
Use the named getters instead of creating DatabaseManager directly:
db = DatabaseRegistry.get_config_database()
db = DatabaseRegistry.get_cache_database()
This class guarantees you will always get the correct database instance.
"""
# Explicit database types
TYPE_CONFIG = 'config'
TYPE_CACHE = 'cache'
# Singleton instances
_instances: Dict[str, DatabaseManager] = {}
@classmethod
def get_config_database(cls, db_config: Optional[Dict[str, Any]] = None) -> DatabaseManager:
"""Get the CONFIGURATION database (aisbf.db) - FOR PERMANENT USER DATA ONLY
NOTE: Config database ALWAYS uses SQLite or MySQL.
Redis is NEVER used for permanent configuration storage.
"""
if cls.TYPE_CONFIG not in cls._instances:
if db_config is not None:
# CONFIG DATABASE NEVER USES REDIS - FORCE SQLITE OR MYSQL
config_type = db_config.get('type', 'sqlite').lower()
if config_type == 'redis':
logger.warning("⚠️ CONFIG DATABASE: Redis requested, falling back to SQLite (Redis is for cache only!)")
# Fallback to SQLite for config
aisbf_dir = Path.home() / '.aisbf'
aisbf_dir.mkdir(exist_ok=True)
db_config = {
'type': 'sqlite',
'sqlite_path': str(aisbf_dir / 'aisbf.db'),
'mysql_host': 'localhost',
'mysql_port': 3306,
'mysql_user': 'aisbf',
'mysql_password': '',
'mysql_database': 'aisbf'
}
cls._instances[cls.TYPE_CONFIG] = DatabaseManager(db_config, database_type=cls.TYPE_CONFIG)
logger.info(f"✅ CONFIG DATABASE INSTANCE REGISTERED [backend: {cls._instances[cls.TYPE_CONFIG].db_type}]")
return cls._instances[cls.TYPE_CONFIG]
@classmethod
def get_cache_database(cls, db_config: Optional[Dict[str, Any]] = None) -> DatabaseManager:
"""Get the CACHE database (cache.db) - FOR TEMPORARY DATA ONLY"""
if cls.TYPE_CACHE not in cls._instances:
# For cache database, respect the configured backend type
if db_config is None:
aisbf_dir = Path.home() / '.aisbf'
aisbf_dir.mkdir(exist_ok=True)
# Use same backend type as config database by default
db_config = {
'type': 'sqlite',
'sqlite_path': str(aisbf_dir / 'cache.db'),
'mysql_host': 'localhost',
'mysql_port': 3306,
'mysql_user': 'aisbf',
'mysql_password': '',
'mysql_database': 'aisbf_cache',
'redis_host': 'localhost',
'redis_port': 6379,
'redis_db': 1,
'redis_password': '',
'redis_key_prefix': 'aisbf:cache:'
}
else:
# Ensure we always use the correct database name/suffix for cache
if db_config.get('type') == 'mysql':
if 'mysql_database' in db_config:
# Never use main config database for cache
if db_config['mysql_database'] == 'aisbf':
db_config['mysql_database'] = 'aisbf_cache'
if db_config.get('type') == 'redis':
# Use separate Redis DB number for cache
if db_config.get('redis_db', 0) == 0:
db_config['redis_db'] = 1
if 'redis_key_prefix' not in db_config:
db_config['redis_key_prefix'] = 'aisbf:cache:'
cls._instances[cls.TYPE_CACHE] = DatabaseManager(db_config, database_type=cls.TYPE_CACHE)
logger.info(f"✅ CACHE DATABASE INSTANCE REGISTERED [backend: {db_config.get('type', 'sqlite')}]")
return cls._instances[cls.TYPE_CACHE]
@classmethod
def get_instance(cls, database_type: str, db_config: Optional[Dict[str, Any]] = None) -> DatabaseManager:
"""Get database instance by explicit type"""
if database_type == cls.TYPE_CONFIG:
return cls.get_config_database(db_config)
elif database_type == cls.TYPE_CACHE:
return cls.get_cache_database(db_config)
else:
raise ValueError(f"Unknown database type: {database_type}. Use TYPE_CONFIG or TYPE_CACHE")
@classmethod
def reset(cls):
"""Reset all instances (for testing only)"""
cls._instances.clear()
# ============================================================
# LEGACY COMPATIBILITY LAYER
# ============================================================
def get_database(db_config: Optional[Dict[str, Any]] = None) -> DatabaseManager:
"""
Legacy getter - DEPRECATED!
Use DatabaseRegistry.get_config_database() instead for explicit type safety.
This method now returns ONLY the CONFIG database to preserve existing behaviour.
"""
logger.warning("⚠️ DEPRECATED: get_database() is deprecated. Use DatabaseRegistry.get_config_database() or DatabaseRegistry.get_cache_database() instead")
return DatabaseRegistry.get_config_database(db_config)
def initialize_database(db_config: Optional[Dict[str, Any]] = None):
"""
Initialize the database and clean up old records.
This should be called at application startup.
Args:
db_config: Database configuration. If None, uses default.
"""
db = DatabaseRegistry.get_config_database(db_config)
db.cleanup_old_token_usage(days_to_keep=7)
logger.info("Database initialized and old records cleaned up")
# Now modify DatabaseManager constructor to accept type parameter
def DatabaseManager__init__(self, db_config: Optional[Dict[str, Any]] = None, database_type: str = DatabaseRegistry.TYPE_CONFIG):
"""
Initialize the database manager.
Args:
db_config: Database configuration dictionary. If None, uses default SQLite config.
database_type: TYPE_CONFIG for permanent user data, TYPE_CACHE for temporary cache
"""
self.database_type = database_type
if db_config is None:
# Default SQLite configuration
aisbf_dir = Path.home() / '.aisbf'
aisbf_dir.mkdir(exist_ok=True)
if database_type == DatabaseRegistry.TYPE_CONFIG:
db_path = str(aisbf_dir / 'aisbf.db')
else:
db_path = str(aisbf_dir / 'cache.db')
self.db_config = {
'type': 'sqlite',
'sqlite_path': db_path,
'mysql_host': 'localhost',
'mysql_port': 3306,
'mysql_user': 'aisbf',
'mysql_password': '',
'mysql_database': 'aisbf' if database_type == DatabaseRegistry.TYPE_CONFIG else 'aisbf_cache'
}
else:
self.db_config = db_config
self.db_type = self.db_config.get('type', 'sqlite').lower()
if self.db_type == 'mysql' and not MYSQL_AVAILABLE:
raise ImportError("MySQL connector not available. Install mysql-connector-python.")
self._initialize_database()
logger.info(f"Database initialized: {self.db_type} [TYPE: {self.database_type}]")
# Patch the constructor
DatabaseManager.__init__ = DatabaseManager__init__
# ============================================================
# SAFETY MECHANISM: Prevent config tables in cache database
# ============================================================
def DatabaseManager__initialize_database(self):
"""Create database tables if they don't exist."""
with self._get_connection() as conn:
cursor = conn.cursor()
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')
# Set busy timeout to 5 seconds for concurrent access
cursor.execute('PRAGMA busy_timeout=5000')
auto_increment = 'AUTOINCREMENT'
timestamp_default = 'CURRENT_TIMESTAMP'
boolean_type = 'BOOLEAN'
else: # mysql
auto_increment = 'AUTO_INCREMENT'
timestamp_default = 'CURRENT_TIMESTAMP'
boolean_type = 'TINYINT(1)'
# ==============================================
# SAFETY CHECK: NEVER CREATE USER TABLES IN CACHE DB
# ==============================================
if self.database_type == DatabaseRegistry.TYPE_CONFIG:
# ONLY CREATE CONFIG TABLES IN CONFIG DATABASE
self._create_config_tables(cursor, auto_increment, timestamp_default, boolean_type)
self._run_config_migrations(cursor)
else:
# CACHE DATABASE GETS MINIMAL TABLES ONLY
self._create_cache_tables(cursor, auto_increment, timestamp_default, boolean_type)
conn.commit()
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)"""
# 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'''
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}
)
''')
# 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:
cursor.execute('''
CREATE INDEX IF NOT EXISTS idx_model_embeddings_provider_model
ON model_embeddings(provider_id, model_name)
''')
except:
pass
# 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,
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
)
''')
# User-specific configuration tables for multi-user isolation
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)
)
''')
def DatabaseManager__create_cache_tables(self, cursor, auto_increment, timestamp_default, boolean_type):
"""Create only temporary cache tables (CACHE DB ONLY)"""
# Only minimal tracking tables for cache database
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")
def DatabaseManager__run_config_migrations(self, cursor):
"""Run all configuration database migrations"""
# ==============================================
# UNIVERSAL MIGRATIONS - RUN ON EVERY STARTUP
# These run for ALL databases, new and existing
# ==============================================
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")
else:
cursor.execute("""
SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_NAME = 'account_tiers'
""")
if not cursor.fetchone():
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')
]
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")
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: 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)
)
'''),
('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")
# Patch the methods
DatabaseManager._initialize_database = DatabaseManager__initialize_database
DatabaseManager._create_config_tables = DatabaseManager__create_config_tables
DatabaseManager._create_cache_tables = DatabaseManager__create_cache_tables
DatabaseManager._run_config_migrations = DatabaseManager__run_config_migrations
......@@ -88,3 +88,68 @@ class ErrorTracking(BaseModel):
failures: int
last_failure: Optional[int]
disabled_until: Optional[int]
class AccountTier(BaseModel):
id: Optional[int] = None
name: str
description: Optional[str] = None
price_monthly: float = 0.0
price_yearly: float = 0.0
is_default: bool = False
is_active: bool = True
# Limits
max_requests_per_day: int = -1
max_requests_per_month: int = -1
max_providers: int = -1
max_rotations: int = -1
max_autoselections: int = -1
max_rotation_models: int = -1
max_autoselection_models: int = -1
created_at: Optional[int] = None
updated_at: Optional[int] = None
class UserSubscription(BaseModel):
id: Optional[int] = None
user_id: int
tier_id: int
status: str = "active" # active, canceled, expired, suspended
start_date: int
end_date: Optional[int] = None
next_billing_date: Optional[int] = None
trial_end_date: Optional[int] = None
payment_method_id: Optional[int] = None
auto_renew: bool = True
created_at: Optional[int] = None
updated_at: Optional[int] = None
class PaymentMethod(BaseModel):
id: Optional[int] = None
user_id: int
type: str # paypal, stripe, bitcoin, eth, usdt, usdc
identifier: str
is_default: bool = False
is_active: bool = True
metadata: Optional[Dict] = None
created_at: Optional[int] = None
updated_at: Optional[int] = None
class PaymentTransaction(BaseModel):
id: Optional[int] = None
user_id: int
tier_id: Optional[int] = None
subscription_id: Optional[int] = None
payment_method_id: Optional[int] = None
amount: float
currency: str = "USD"
status: str # pending, completed, failed, refunded
transaction_type: str # subscription, one_time, renewal
external_transaction_id: Optional[str] = None
metadata: Optional[Dict] = None
created_at: Optional[int] = None
completed_at: Optional[int] = None
......@@ -146,5 +146,49 @@
"client_secret": "",
"scopes": ["user:email", "read:user"]
}
},
"billing": {
"currency": "USD",
"currency_symbol": "$",
"currency_decimals": 2,
"payment_methods": {
"paypal": {
"enabled": false,
"client_id": "",
"client_secret": "",
"mode": "sandbox"
},
"stripe": {
"enabled": false,
"publishable_key": "",
"secret_key": "",
"webhook_secret": "",
"mode": "test"
},
"bitcoin": {
"enabled": false,
"wallet_address": "",
"confirmations_required": 3
},
"eth": {
"enabled": false,
"wallet_address": "",
"chain_id": 1,
"confirmations_required": 12
},
"usdt": {
"enabled": false,
"wallet_address": "",
"chain": "ethereum",
"confirmations_required": 12
},
"usdc": {
"enabled": false,
"wallet_address": "",
"chain": "ethereum",
"confirmations_required": 12
}
}
}
}
#!/usr/bin/env python3
"""
Database diagnostic script for AISBF
This script checks which database contains which tables and helps identify
database separation issues.
Copyright (C) 2026 Stefy Lanza <stefy@nexlab.net>
"""
import sqlite3
import sys
from pathlib import Path
from typing import List, Tuple
def get_tables(db_path: Path) -> List[str]:
"""Get list of tables in a database"""
if not db_path.exists():
return []
try:
with sqlite3.connect(str(db_path)) as conn:
cursor = conn.cursor()
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name")
return [row[0] for row in cursor.fetchall()]
except Exception as e:
print(f"Error reading {db_path}: {e}")
return []
def get_table_count(db_path: Path, table_name: str) -> int:
"""Get row count for a table"""
if not db_path.exists():
return 0
try:
with sqlite3.connect(str(db_path)) as conn:
cursor = conn.cursor()
cursor.execute(f"SELECT COUNT(*) FROM {table_name}")
return cursor.fetchone()[0]
except Exception as e:
return 0
def main():
print("=" * 70)
print("AISBF Database Diagnostic Tool")
print("=" * 70)
print()
# Database paths
aisbf_dir = Path.home() / '.aisbf'
cache_db = aisbf_dir / 'cache.db'
aisbf_db = aisbf_dir / 'aisbf.db'
response_cache_db = aisbf_dir / 'response_cache.db'
print(f"Checking databases in: {aisbf_dir}")
print()
# Configuration tables (should be in aisbf.db)
config_tables = [
'users', 'user_providers', 'user_rotations', 'user_autoselects',
'user_prompts', 'user_api_tokens', 'user_token_usage',
'user_auth_files', 'user_oauth2_credentials',
'account_tiers', 'payment_methods', 'user_subscriptions',
'payment_transactions', 'context_dimensions', 'token_usage',
'model_embeddings'
]
# Cache tables (should be in cache.db)
cache_tables = ['cache', 'response_cache']
# Check cache.db
print("📁 cache.db")
print("-" * 70)
if cache_db.exists():
cache_db_tables = get_tables(cache_db)
print(f" Tables found: {len(cache_db_tables)}")
# Check for misplaced configuration tables
misplaced = [t for t in cache_db_tables if t in config_tables]
if misplaced:
print(f" ⚠️ WARNING: Configuration tables found in cache.db:")
for table in misplaced:
count = get_table_count(cache_db, table)
print(f" - {table} ({count} rows)")
# Check for correct cache tables
correct = [t for t in cache_db_tables if t in cache_tables]
if correct:
print(f" ✅ Cache tables (correct):")
for table in correct:
count = get_table_count(cache_db, table)
print(f" - {table} ({count} rows)")
# Unknown tables
unknown = [t for t in cache_db_tables if t not in config_tables and t not in cache_tables]
if unknown:
print(f" ❓ Unknown tables:")
for table in unknown:
count = get_table_count(cache_db, table)
print(f" - {table} ({count} rows)")
else:
print(" ❌ Database does not exist")
print()
# Check aisbf.db
print("📁 aisbf.db")
print("-" * 70)
if aisbf_db.exists():
aisbf_db_tables = get_tables(aisbf_db)
print(f" Tables found: {len(aisbf_db_tables)}")
# Check for correct configuration tables
correct = [t for t in aisbf_db_tables if t in config_tables]
if correct:
print(f" ✅ Configuration tables (correct):")
for table in correct:
count = get_table_count(aisbf_db, table)
print(f" - {table} ({count} rows)")
# Check for misplaced cache tables
misplaced = [t for t in aisbf_db_tables if t in cache_tables]
if misplaced:
print(f" ⚠️ WARNING: Cache tables found in aisbf.db:")
for table in misplaced:
count = get_table_count(aisbf_db, table)
print(f" - {table} ({count} rows)")
# Unknown tables
unknown = [t for t in aisbf_db_tables if t not in config_tables and t not in cache_tables]
if unknown:
print(f" ❓ Unknown tables:")
for table in unknown:
count = get_table_count(aisbf_db, table)
print(f" - {table} ({count} rows)")
else:
print(" ❌ Database does not exist")
print()
# Check response_cache.db
print("📁 response_cache.db")
print("-" * 70)
if response_cache_db.exists():
response_cache_db_tables = get_tables(response_cache_db)
print(f" Tables found: {len(response_cache_db_tables)}")
for table in response_cache_db_tables:
count = get_table_count(response_cache_db, table)
print(f" - {table} ({count} rows)")
else:
print(" ❌ Database does not exist")
print()
# Summary and recommendations
print("=" * 70)
print("Summary")
print("=" * 70)
issues_found = False
# Check for configuration tables in cache.db
if cache_db.exists():
cache_db_tables = get_tables(cache_db)
misplaced_in_cache = [t for t in cache_db_tables if t in config_tables]
if misplaced_in_cache:
issues_found = True
print("❌ ISSUE: Configuration tables found in cache.db")
print(f" Tables: {', '.join(misplaced_in_cache)}")
print()
print(" SOLUTION: Run the migration script:")
print(" python migrate_cache_to_aisbf.py")
print()
# Check for cache tables in aisbf.db
if aisbf_db.exists():
aisbf_db_tables = get_tables(aisbf_db)
misplaced_in_aisbf = [t for t in aisbf_db_tables if t in cache_tables]
if misplaced_in_aisbf:
issues_found = True
print("⚠️ WARNING: Cache tables found in aisbf.db")
print(f" Tables: {', '.join(misplaced_in_aisbf)}")
print(" This is unusual but not critical.")
print()
if not issues_found:
print("✅ All tables are in the correct databases!")
print()
print("Database separation is correct:")
print(" - aisbf.db contains configuration tables")
print(" - cache.db contains cache tables only")
print()
print("=" * 70)
return 0 if not issues_found else 1
if __name__ == '__main__':
sys.exit(main())
......@@ -1265,6 +1265,190 @@ async def auth_middleware(request: Request, call_next):
response = await call_next(request)
return response
# Account Tier Limit Enforcement Middleware
@app.middleware("http")
async def tier_limit_middleware(request: Request, call_next):
"""Validate user account tier limits before processing API requests"""
# Skip tier checks for non-API paths
if (request.url.path == "/" or
request.url.path.startswith("/dashboard") or
request.url.path == "/favicon.ico" or
request.url.path.startswith("/.well-known/") or
request.url.path.startswith("/mcp") or
request.url.path.startswith("/auth/")):
return await call_next(request)
# Skip tier checks for GET models endpoints
if request.method == "GET" and (request.url.path.endswith("/models") or request.url.path.endswith("/models/")):
return await call_next(request)
# Only apply tier limits for authenticated users
user_id = getattr(request.state, 'user_id', None)
if not user_id:
return await call_next(request)
from aisbf.database import get_database
db = get_database()
# Get user tier and current usage
tier = db.get_user_tier(user_id)
if not tier:
# Default free tier - allow all requests
return await call_next(request)
# Check if subscription is active and not expired
subscription = db.get_user_subscription(user_id)
if subscription:
from datetime import datetime
if subscription['expires_at'] and datetime.fromisoformat(subscription['expires_at']) < datetime.now():
return JSONResponse(
status_code=402,
content={
"error": "Subscription expired",
"message": "Your subscription has expired. Please renew to continue using the service.",
"code": "subscription_expired"
}
)
# Get current usage statistics
usage = db.get_user_usage(user_id)
# Validate all tier limits with standard semantics:
# -1 = unlimited, 0 = blocked, >0 = actual limit
# 1. Max requests per day
if tier['max_requests_per_day'] == 0:
return JSONResponse(
status_code=402,
content={
"error": "Requests not permitted",
"message": "Your account tier does not allow API requests. Please upgrade your plan.",
"code": "requests_blocked"
}
)
elif tier['max_requests_per_day'] > 0 and usage['requests_today'] >= tier['max_requests_per_day']:
return JSONResponse(
status_code=429,
content={
"error": "Daily request limit exceeded",
"message": f"You have reached your daily request limit of {tier['max_requests_per_day']} requests. Upgrade your plan for higher limits.",
"limit": tier['max_requests_per_day'],
"current": usage['requests_today'],
"code": "daily_limit_exceeded"
}
)
# 2. Max requests per month
if tier['max_requests_per_month'] == 0:
return JSONResponse(
status_code=402,
content={
"error": "Requests not permitted",
"message": "Your account tier does not allow API requests. Please upgrade your plan.",
"code": "requests_blocked"
}
)
elif tier['max_requests_per_month'] > 0 and usage['requests_month'] >= tier['max_requests_per_month']:
return JSONResponse(
status_code=429,
content={
"error": "Monthly request limit exceeded",
"message": f"You have reached your monthly request limit of {tier['max_requests_per_month']} requests. Upgrade your plan for higher limits.",
"limit": tier['max_requests_per_month'],
"current": usage['requests_month'],
"code": "monthly_limit_exceeded"
}
)
# 3. Max providers check (only when creating providers)
if request.url.path.endswith("/dashboard/user/providers") and request.method == "POST":
if tier['max_providers'] == 0:
return JSONResponse(
status_code=403,
content={
"error": "Provider creation not permitted",
"message": "Your account tier does not allow configuring providers. Please upgrade your plan.",
"code": "providers_blocked"
}
)
elif tier['max_providers'] > 0 and usage['providers_count'] >= tier['max_providers']:
return JSONResponse(
status_code=403,
content={
"error": "Maximum providers limit reached",
"message": f"You have reached your limit of {tier['max_providers']} providers. Upgrade your plan for higher limits.",
"limit": tier['max_providers'],
"current": usage['providers_count'],
"code": "providers_limit_exceeded"
}
)
# 4. Max rotations check (only when creating rotations)
if request.url.path.endswith("/dashboard/user/rotations") and request.method == "POST":
if tier['max_rotations'] == 0:
return JSONResponse(
status_code=403,
content={
"error": "Rotation creation not permitted",
"message": "Your account tier does not allow configuring rotations. Please upgrade your plan.",
"code": "rotations_blocked"
}
)
elif tier['max_rotations'] > 0 and usage['rotations_count'] >= tier['max_rotations']:
return JSONResponse(
status_code=403,
content={
"error": "Maximum rotations limit reached",
"message": f"You have reached your limit of {tier['max_rotations']} rotations. Upgrade your plan for higher limits.",
"limit": tier['max_rotations'],
"current": usage['rotations_count'],
"code": "rotations_limit_exceeded"
}
)
# 5. Max autoselections check (only when creating autoselects)
if request.url.path.endswith("/dashboard/user/autoselects") and request.method == "POST":
if tier['max_autoselections'] == 0:
return JSONResponse(
status_code=403,
content={
"error": "Autoselection creation not permitted",
"message": "Your account tier does not allow configuring autoselections. Please upgrade your plan.",
"code": "autoselections_blocked"
}
)
elif tier['max_autoselections'] > 0 and usage['autoselects_count'] >= tier['max_autoselections']:
return JSONResponse(
status_code=403,
content={
"error": "Maximum autoselections limit reached",
"message": f"You have reached your limit of {tier['max_autoselections']} autoselections. Upgrade your plan for higher limits.",
"limit": tier['max_autoselections'],
"current": usage['autoselects_count'],
"code": "autoselections_limit_exceeded"
}
)
# All limits passed, process request
response = await call_next(request)
# Record request usage after successful processing
if request.method == "POST" and (
request.url.path.endswith("/chat/completions") or
request.url.path.endswith("/completions") or
request.url.path.endswith("/embeddings") or
request.url.path.endswith("/audio/transcriptions") or
request.url.path.endswith("/audio/speech") or
request.url.path.endswith("/images/generations")
):
# Increment request counters asynchronously
import asyncio
asyncio.create_task(db.increment_user_request_count(user_id))
return response
async def record_token_usage_async(user_id: int, token_id: int):
"""Asynchronously record token usage"""
try:
......@@ -4467,6 +4651,419 @@ async def dashboard_response_cache_stats(request: Request):
'error': str(e)
})
@app.get("/dashboard/admin/tiers")
async def dashboard_admin_tiers(request: Request):
"""Admin account tiers management page"""
auth_check = require_admin(request)
if auth_check:
return auth_check
from aisbf.database import get_database
db = get_database()
tiers = db.get_all_tiers()
return templates.TemplateResponse(
request=request,
name="dashboard/admin_tiers.html",
context={
"request": request,
"session": request.session,
"tiers": tiers
}
)
# API endpoints for tiers CRUD operations
@app.get("/api/admin/tiers")
async def api_list_tiers(request: Request):
"""List all tiers - API endpoint"""
auth_check = require_admin(request)
if auth_check:
return auth_check
from aisbf.database import get_database
db = get_database()
tiers = db.get_all_tiers()
return JSONResponse(tiers)
@app.get("/api/admin/tiers/{tier_id}")
async def api_get_tier(request: Request, tier_id: int):
"""Get a specific tier by ID - API endpoint"""
auth_check = require_admin(request)
if auth_check:
return auth_check
from aisbf.database import get_database
db = get_database()
tier = db.get_tier_by_id(tier_id)
if not tier:
return JSONResponse({"error": "Tier not found"}, status_code=404)
return JSONResponse(tier)
@app.post("/api/admin/tiers")
async def api_create_tier(request: Request):
"""Create a new tier - API endpoint"""
auth_check = require_admin(request)
if auth_check:
return auth_check
from aisbf.database import get_database
db = get_database()
try:
body = await request.json()
tier_id = db.create_tier(
name=body.get('name'),
description=body.get('description', ''),
price_monthly=body.get('price_monthly', 0.0),
price_yearly=body.get('price_yearly', 0.0),
max_requests_per_day=body.get('max_requests_per_day', -1),
max_requests_per_month=body.get('max_requests_per_month', -1),
max_providers=body.get('max_providers', -1),
max_rotations=body.get('max_rotations', -1),
max_autoselections=body.get('max_autoselections', -1),
max_rotation_models=body.get('max_rotation_models', -1),
max_autoselection_models=body.get('max_autoselection_models', -1),
is_active=body.get('is_active', True)
)
return JSONResponse({"success": True, "tier_id": tier_id})
except Exception as e:
logger.error(f"Error creating tier: {e}")
return JSONResponse({"error": str(e)}, status_code=500)
@app.put("/api/admin/tiers/{tier_id}")
async def api_update_tier(request: Request, tier_id: int):
"""Update an existing tier - API endpoint"""
auth_check = require_admin(request)
if auth_check:
return auth_check
from aisbf.database import get_database
db = get_database()
try:
body = await request.json()
# Build update kwargs
update_kwargs = {}
if 'name' in body:
update_kwargs['name'] = body['name']
if 'description' in body:
update_kwargs['description'] = body['description']
if 'price_monthly' in body:
update_kwargs['price_monthly'] = body['price_monthly']
if 'price_yearly' in body:
update_kwargs['price_yearly'] = body['price_yearly']
if 'max_requests_per_day' in body:
update_kwargs['max_requests_per_day'] = body['max_requests_per_day']
if 'max_requests_per_month' in body:
update_kwargs['max_requests_per_month'] = body['max_requests_per_month']
if 'max_providers' in body:
update_kwargs['max_providers'] = body['max_providers']
if 'max_rotations' in body:
update_kwargs['max_rotations'] = body['max_rotations']
if 'max_autoselections' in body:
update_kwargs['max_autoselections'] = body['max_autoselections']
if 'max_rotation_models' in body:
update_kwargs['max_rotation_models'] = body['max_rotation_models']
if 'max_autoselection_models' in body:
update_kwargs['max_autoselection_models'] = body['max_autoselection_models']
if 'is_active' in body:
update_kwargs['is_active'] = body['is_active']
success = db.update_tier(tier_id, **update_kwargs)
if not success:
return JSONResponse({"error": "Tier not found or no changes"}, status_code=404)
return JSONResponse({"success": True})
except Exception as e:
logger.error(f"Error updating tier: {e}")
return JSONResponse({"error": str(e)}, status_code=500)
@app.delete("/api/admin/tiers/{tier_id}")
async def api_delete_tier(request: Request, tier_id: int):
"""Delete a tier - API endpoint"""
auth_check = require_admin(request)
if auth_check:
return auth_check
from aisbf.database import get_database
db = get_database()
try:
success = db.delete_tier(tier_id)
if not success:
return JSONResponse({"error": "Cannot delete default tier or tier not found"}, status_code=400)
return JSONResponse({"success": True})
except Exception as e:
logger.error(f"Error deleting tier: {e}")
return JSONResponse({"error": str(e)}, status_code=500)
# Tier form pages
@app.get("/dashboard/admin/tiers/create")
async def dashboard_admin_tier_create(request: Request):
"""Create tier page"""
auth_check = require_admin(request)
if auth_check:
return auth_check
return templates.TemplateResponse(
request=request,
name="dashboard/admin_tier_form.html",
context={
"request": request,
"session": request.session,
"tier": None
}
)
@app.get("/dashboard/admin/tiers/edit/{tier_id}")
async def dashboard_admin_tier_edit(request: Request, tier_id: int):
"""Edit tier page"""
auth_check = require_admin(request)
if auth_check:
return auth_check
from aisbf.database import get_database
db = get_database()
tier = db.get_tier_by_id(tier_id)
if not tier:
return RedirectResponse(url=url_for(request, "/dashboard/admin/tiers"), status_code=303)
return templates.TemplateResponse(
request=request,
name="dashboard/admin_tier_form.html",
context={
"request": request,
"session": request.session,
"tier": tier
}
)
@app.post("/dashboard/admin/tiers/save")
async def dashboard_admin_tier_save(request: Request):
"""Save tier (create or update)"""
auth_check = require_admin(request)
if auth_check:
return auth_check
from aisbf.database import get_database
db = get_database()
try:
form = await request.form()
tier_id = form.get('tier_id')
tier_data = {
'name': form.get('name'),
'description': form.get('description', ''),
'price_monthly': float(form.get('price_monthly', 0)),
'price_yearly': float(form.get('price_yearly', 0)),
'max_requests_per_day': int(form.get('max_requests_per_day', -1)),
'max_requests_per_month': int(form.get('max_requests_per_month', -1)),
'max_providers': int(form.get('max_providers', -1)),
'max_rotations': int(form.get('max_rotations', -1)),
'max_autoselections': int(form.get('max_autoselections', -1)),
'max_rotation_models': int(form.get('max_rotation_models', -1)),
'max_autoselection_models': int(form.get('max_autoselection_models', -1)),
'is_active': form.get('is_active') == '1'
}
if tier_id:
# Update existing tier
db.update_tier(int(tier_id), **tier_data)
else:
# Create new tier
db.create_tier(**tier_data)
return RedirectResponse(url=url_for(request, "/dashboard/admin/tiers"), status_code=303)
except Exception as e:
logger.error(f"Error saving tier: {e}")
return RedirectResponse(url=url_for(request, "/dashboard/admin/tiers"), status_code=303)
# Currency settings endpoints
@app.get("/api/admin/settings/currency")
async def api_get_currency_settings(request: Request):
"""Get currency settings - API endpoint"""
auth_check = require_admin(request)
if auth_check:
return auth_check
# Return default currency settings
# These could be stored in database or config file in the future
return JSONResponse({
"currency_code": "USD",
"currency_symbol": "$",
"currency_decimals": 2
})
@app.post("/api/admin/settings/currency")
async def api_save_currency_settings(request: Request):
"""Save currency settings - API endpoint"""
auth_check = require_admin(request)
if auth_check:
return auth_check
try:
body = await request.json()
# In the future, save to database or config file
# For now, just return success
return JSONResponse({"success": True, "message": "Currency settings saved"})
except Exception as e:
logger.error(f"Error saving currency settings: {e}")
return JSONResponse({"error": str(e)}, status_code=500)
# Payment gateway settings endpoints
@app.get("/api/admin/settings/payment-gateways")
async def api_get_payment_gateways(request: Request):
"""Get payment gateway settings - API endpoint"""
auth_check = require_admin(request)
if auth_check:
return auth_check
# Return default/empty payment gateway settings
# These could be stored in database or config file in the future
return JSONResponse({
"paypal": {"enabled": False, "client_id": "", "client_secret": "", "webhook_secret": "", "sandbox": True},
"stripe": {"enabled": False, "publishable_key": "", "secret_key": "", "webhook_secret": "", "sandbox": True},
"bitcoin": {"enabled": False, "address": "", "confirmations": 3, "expiration_minutes": 120},
"ethereum": {"enabled": False, "address": "", "confirmations": 12, "chain_id": 1},
"usdt": {"enabled": False, "address": "", "network": "erc20", "confirmations": 3},
"usdc": {"enabled": False, "address": "", "network": "erc20", "confirmations": 3}
})
@app.post("/api/admin/settings/payment-gateways")
async def api_save_payment_gateways(request: Request):
"""Save payment gateway settings - API endpoint"""
auth_check = require_admin(request)
if auth_check:
return auth_check
try:
body = await request.json()
# In the future, save to database or config file
# For now, just return success
return JSONResponse({"success": True, "message": "Payment gateway settings saved"})
except Exception as e:
logger.error(f"Error saving payment gateway settings: {e}")
return JSONResponse({"error": str(e)}, status_code=500)
@app.get("/dashboard/pricing")
async def dashboard_pricing(request: Request):
"""Pricing plans page for users"""
auth_check = require_dashboard_auth(request)
if auth_check:
return auth_check
from aisbf.database import get_database
db = get_database()
tiers = db.get_all_tiers()
current_tier = db.get_user_tier(request.session.get('user_id'))
# Get enabled payment gateways
enabled_gateways = []
from aisbf.config import config
if config.aisbf and hasattr(config.aisbf, 'payment_gateways') and config.aisbf.payment_gateways:
for gateway, settings in config.aisbf.payment_gateways.items():
if settings.get('enabled', False):
enabled_gateways.append(gateway)
# Get currency settings
currency_symbol = "$"
if config.aisbf and config.aisbf.currency:
currency_symbol = config.aisbf.currency.get('symbol', "$")
return templates.TemplateResponse(
request=request,
name="dashboard/pricing.html",
context={
"request": request,
"session": request.session,
"tiers": tiers,
"current_tier": current_tier,
"enabled_gateways": enabled_gateways,
"currency_symbol": currency_symbol
}
)
@app.get("/dashboard/subscription")
async def dashboard_subscription(request: Request):
"""User subscription status and payment methods management page"""
auth_check = require_dashboard_auth(request)
if auth_check:
return auth_check
from aisbf.database import get_database
db = get_database()
user_id = request.session.get('user_id')
# Get user subscription info
subscription = db.get_user_subscription(user_id)
current_tier = db.get_user_tier(user_id)
payment_methods = db.get_user_payment_methods(user_id)
# Get enabled payment gateways
enabled_gateways = []
from aisbf.config import config
if config.aisbf and hasattr(config.aisbf, 'payment_gateways') and config.aisbf.payment_gateways:
for gateway, settings in config.aisbf.payment_gateways.items():
if settings.get('enabled', False):
enabled_gateways.append(gateway)
# Get currency settings
currency_symbol = "$"
if config.aisbf and config.aisbf.currency:
currency_symbol = config.aisbf.currency.get('symbol', "$")
return templates.TemplateResponse(
request=request,
name="dashboard/subscription.html",
context={
"request": request,
"session": request.session,
"subscription": subscription,
"current_tier": current_tier,
"payment_methods": payment_methods,
"enabled_gateways": enabled_gateways,
"currency_symbol": currency_symbol
}
)
@app.get("/dashboard/billing")
async def dashboard_billing(request: Request):
"""User payment transaction history page"""
auth_check = require_dashboard_auth(request)
if auth_check:
return auth_check
from aisbf.database import get_database
db = get_database()
user_id = request.session.get('user_id')
transactions = db.get_user_payment_transactions(user_id)
return templates.TemplateResponse(
request=request,
name="dashboard/billing.html",
context={
"request": request,
"session": request.session,
"transactions": transactions
}
)
@app.get("/dashboard/rate-limits")
async def dashboard_rate_limits(request: Request):
"""Rate limits dashboard page"""
......
#!/usr/bin/env python3
"""
Migration script to move configuration data from cache.db to aisbf.db
This script handles the case where configuration tables (users, providers, etc.)
were incorrectly created in cache.db instead of aisbf.db.
Copyright (C) 2026 Stefy Lanza <stefy@nexlab.net>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
"""
import sqlite3
import sys
from pathlib import Path
import shutil
from datetime import datetime
# Configuration tables that should be in aisbf.db
CONFIG_TABLES = [
'users',
'user_providers',
'user_rotations',
'user_autoselects',
'user_prompts',
'user_api_tokens',
'user_token_usage',
'user_auth_files',
'user_oauth2_credentials',
'account_tiers',
'payment_methods',
'user_subscriptions',
'payment_transactions',
'context_dimensions',
'token_usage',
'model_embeddings'
]
# Cache tables that should stay in cache.db
CACHE_TABLES = [
'cache',
'response_cache'
]
def backup_database(db_path: Path) -> Path:
"""Create a backup of the database"""
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
backup_path = db_path.parent / f"{db_path.stem}_backup_{timestamp}{db_path.suffix}"
shutil.copy2(db_path, backup_path)
print(f"✅ Created backup: {backup_path}")
return backup_path
def get_table_list(conn: sqlite3.Connection) -> list:
"""Get list of tables in database"""
cursor = conn.cursor()
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")
return [row[0] for row in cursor.fetchall()]
def table_exists(conn: sqlite3.Connection, table_name: str) -> bool:
"""Check if table exists in database"""
cursor = conn.cursor()
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name=?", (table_name,))
return cursor.fetchone() is not None
def get_table_schema(conn: sqlite3.Connection, table_name: str) -> str:
"""Get CREATE TABLE statement for a table"""
cursor = conn.cursor()
cursor.execute("SELECT sql FROM sqlite_master WHERE type='table' AND name=?", (table_name,))
result = cursor.fetchone()
return result[0] if result else None
def copy_table_data(source_conn: sqlite3.Connection, dest_conn: sqlite3.Connection,
table_name: str, force: bool = False) -> tuple:
"""
Copy table data from source to destination database
Returns:
(success: bool, rows_copied: int, message: str)
"""
try:
# Check if table exists in source
if not table_exists(source_conn, table_name):
return (True, 0, f"Table '{table_name}' not found in source database (skipping)")
# Get row count from source
source_cursor = source_conn.cursor()
source_cursor.execute(f"SELECT COUNT(*) FROM {table_name}")
source_count = source_cursor.fetchone()[0]
if source_count == 0:
return (True, 0, f"Table '{table_name}' is empty in source database (skipping)")
# Check if table exists in destination
dest_exists = table_exists(dest_conn, table_name)
if dest_exists:
# Get row count from destination
dest_cursor = dest_conn.cursor()
dest_cursor.execute(f"SELECT COUNT(*) FROM {table_name}")
dest_count = dest_cursor.fetchone()[0]
if dest_count > 0 and not force:
return (False, 0, f"Table '{table_name}' already has {dest_count} rows in destination. Use --force to overwrite.")
# Get table schema from source
schema = get_table_schema(source_conn, table_name)
if not schema:
return (False, 0, f"Could not get schema for table '{table_name}'")
# Create table in destination if it doesn't exist
if not dest_exists:
dest_cursor = dest_conn.cursor()
dest_cursor.execute(schema)
dest_conn.commit()
print(f" Created table '{table_name}' in destination")
# Get all data from source
source_cursor.execute(f"SELECT * FROM {table_name}")
rows = source_cursor.fetchall()
# Get column names
column_names = [description[0] for description in source_cursor.description]
placeholders = ','.join(['?' for _ in column_names])
columns_str = ','.join(column_names)
# Insert data into destination
dest_cursor = dest_conn.cursor()
if force and dest_exists:
# Clear existing data if force mode
dest_cursor.execute(f"DELETE FROM {table_name}")
print(f" Cleared existing data from '{table_name}'")
dest_cursor.executemany(
f"INSERT OR REPLACE INTO {table_name} ({columns_str}) VALUES ({placeholders})",
rows
)
dest_conn.commit()
return (True, len(rows), f"Copied {len(rows)} rows")
except Exception as e:
return (False, 0, f"Error: {str(e)}")
def migrate_databases(cache_db_path: Path, aisbf_db_path: Path, force: bool = False, dry_run: bool = False):
"""
Migrate configuration tables from cache.db to aisbf.db
Args:
cache_db_path: Path to cache.db
aisbf_db_path: Path to aisbf.db
force: Overwrite existing data in destination
dry_run: Don't actually perform migration, just show what would be done
"""
print("=" * 70)
print("AISBF Database Migration Tool")
print("=" * 70)
print()
# Check if databases exist
if not cache_db_path.exists():
print(f"❌ Error: cache.db not found at {cache_db_path}")
return False
if not aisbf_db_path.exists():
print(f"⚠️ Warning: aisbf.db not found at {aisbf_db_path}")
print(" It will be created during migration.")
print(f"Source (cache.db): {cache_db_path}")
print(f"Destination (aisbf.db): {aisbf_db_path}")
print()
if dry_run:
print("🔍 DRY RUN MODE - No changes will be made")
print()
# Create backups
if not dry_run:
print("Creating backups...")
backup_cache = backup_database(cache_db_path)
if aisbf_db_path.exists():
backup_aisbf = backup_database(aisbf_db_path)
print()
# Connect to databases
cache_conn = sqlite3.connect(str(cache_db_path))
aisbf_conn = sqlite3.connect(str(aisbf_db_path))
try:
# Get list of tables in cache.db
cache_tables = get_table_list(cache_conn)
print(f"Tables found in cache.db: {', '.join(cache_tables)}")
print()
# Find configuration tables that need to be migrated
tables_to_migrate = [t for t in cache_tables if t in CONFIG_TABLES]
if not tables_to_migrate:
print("✅ No configuration tables found in cache.db that need migration.")
print(" All tables are correctly separated.")
return True
print(f"Configuration tables to migrate: {', '.join(tables_to_migrate)}")
print()
# Migrate each table
total_rows = 0
success_count = 0
for table_name in tables_to_migrate:
print(f"Migrating table: {table_name}")
if dry_run:
# Just check what would be done
source_cursor = cache_conn.cursor()
source_cursor.execute(f"SELECT COUNT(*) FROM {table_name}")
count = source_cursor.fetchone()[0]
print(f" Would copy {count} rows")
success_count += 1
total_rows += count
else:
# Actually perform migration
success, rows, message = copy_table_data(cache_conn, aisbf_conn, table_name, force)
print(f" {message}")
if success:
success_count += 1
total_rows += rows
else:
print(f" ❌ Failed to migrate {table_name}")
print()
# Summary
print("=" * 70)
print("Migration Summary")
print("=" * 70)
print(f"Tables processed: {len(tables_to_migrate)}")
print(f"Successfully migrated: {success_count}")
print(f"Total rows migrated: {total_rows}")
if dry_run:
print()
print("This was a dry run. No changes were made.")
print("Run without --dry-run to perform the actual migration.")
else:
print()
print("✅ Migration completed successfully!")
print()
print("Next steps:")
print("1. Verify the migrated data in aisbf.db")
print("2. Test the application to ensure everything works")
print("3. If everything is OK, you can optionally clean up cache.db:")
print(f" python {sys.argv[0]} --cleanup")
return True
except Exception as e:
print(f"❌ Migration failed: {e}")
import traceback
traceback.print_exc()
return False
finally:
cache_conn.close()
aisbf_conn.close()
def cleanup_cache_db(cache_db_path: Path):
"""Remove configuration tables from cache.db after successful migration"""
print("=" * 70)
print("Cleanup cache.db")
print("=" * 70)
print()
if not cache_db_path.exists():
print(f"❌ Error: cache.db not found at {cache_db_path}")
return False
# Create backup first
print("Creating backup...")
backup_cache = backup_database(cache_db_path)
print()
conn = sqlite3.connect(str(cache_db_path))
cursor = conn.cursor()
try:
# Get list of tables
tables = get_table_list(conn)
# Find configuration tables to remove
tables_to_remove = [t for t in tables if t in CONFIG_TABLES]
if not tables_to_remove:
print("✅ No configuration tables found in cache.db")
return True
print(f"Configuration tables to remove: {', '.join(tables_to_remove)}")
print()
# Remove each table
for table_name in tables_to_remove:
print(f"Dropping table: {table_name}")
cursor.execute(f"DROP TABLE IF EXISTS {table_name}")
conn.commit()
# Vacuum to reclaim space
print()
print("Vacuuming database to reclaim space...")
cursor.execute("VACUUM")
print()
print("✅ Cleanup completed successfully!")
print(f" Backup saved at: {backup_cache}")
return True
except Exception as e:
print(f"❌ Cleanup failed: {e}")
import traceback
traceback.print_exc()
return False
finally:
conn.close()
def main():
import argparse
parser = argparse.ArgumentParser(
description='Migrate configuration data from cache.db to aisbf.db',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Dry run to see what would be migrated
python migrate_cache_to_aisbf.py --dry-run
# Perform the migration
python migrate_cache_to_aisbf.py
# Force overwrite existing data
python migrate_cache_to_aisbf.py --force
# Clean up cache.db after successful migration
python migrate_cache_to_aisbf.py --cleanup
# Use custom database paths
python migrate_cache_to_aisbf.py --cache-db /path/to/cache.db --aisbf-db /path/to/aisbf.db
"""
)
parser.add_argument('--cache-db', type=str, default='~/.aisbf/cache.db',
help='Path to cache.db (default: ~/.aisbf/cache.db)')
parser.add_argument('--aisbf-db', type=str, default='~/.aisbf/aisbf.db',
help='Path to aisbf.db (default: ~/.aisbf/aisbf.db)')
parser.add_argument('--force', action='store_true',
help='Overwrite existing data in destination database')
parser.add_argument('--dry-run', action='store_true',
help='Show what would be done without making changes')
parser.add_argument('--cleanup', action='store_true',
help='Remove configuration tables from cache.db after migration')
args = parser.parse_args()
# Expand paths
cache_db_path = Path(args.cache_db).expanduser()
aisbf_db_path = Path(args.aisbf_db).expanduser()
if args.cleanup:
success = cleanup_cache_db(cache_db_path)
else:
success = migrate_databases(cache_db_path, aisbf_db_path, args.force, args.dry_run)
sys.exit(0 if success else 1)
if __name__ == '__main__':
main()
......@@ -49,7 +49,7 @@ class InstallCommand(_install):
setup(
name="aisbf",
version="0.99.20",
version="0.99.21",
author="AISBF Contributors",
author_email="stefy@nexlab.net",
description="AISBF - AI Service Broker Framework || AI Should Be Free - A modular proxy server for managing multiple AI provider integrations",
......@@ -172,6 +172,11 @@ setup(
'templates/dashboard/signup.html',
'templates/dashboard/profile.html',
'templates/dashboard/change_password.html',
'templates/dashboard/admin_tiers.html',
'templates/dashboard/admin_tier_form.html',
'templates/dashboard/pricing.html',
'templates/dashboard/subscription.html',
'templates/dashboard/billing.html',
]),
# Install static files (extension and favicon)
('share/aisbf/static', [
......
......@@ -63,6 +63,47 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
.nav .account-dropdown a:first-child { border-radius: 8px 8px 0 0; }
.nav .account-dropdown a:last-child { border-radius: 0 0 8px 8px; }
.nav .account-dropdown a:hover { background: #0f3460; color: #e0e0e0; }
/* Rainbow Upgrade Button */
.upgrade-button {
font-weight: 700 !important;
padding: 8px 16px !important;
border-radius: 6px !important;
background: linear-gradient(120deg, #ff0080, #ff8c00, #40e0d0, #9370db, #ff0080) !important;
background-size: 400% 400% !important;
animation: rainbow-gradient 3s ease infinite !important;
color: white !important;
text-shadow: 0 1px 2px rgba(0,0,0,0.3) !important;
box-shadow: 0 0 12px rgba(255, 0, 128, 0.5) !important;
margin-left: 4px !important;
transition: transform 0.2s ease, box-shadow 0.2s ease !important;
}
.upgrade-button:hover {
transform: scale(1.05) !important;
box-shadow: 0 0 20px rgba(255, 0, 128, 0.7) !important;
}
@keyframes rainbow-gradient {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
.rainbow-text {
background: linear-gradient(90deg, #ff0000, #ff7f00, #ffff00, #00ff00, #0000ff, #4b0082, #9400d3);
background-size: 400% 400%;
animation: rainbow-text 2s linear infinite;
-webkit-background-clip: text;
background-clip: text;
color: transparent !important;
font-weight: 800 !important;
}
@keyframes rainbow-text {
0% { background-position: 0% center; }
100% { background-position: 400% center; }
}
</style>
<script>
/*
......@@ -90,7 +131,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
document.addEventListener('click', function(event) {
const accountMenu = document.querySelector('.account-menu');
const dropdown = document.getElementById('account-dropdown');
if (!accountMenu.contains(event.target)) {
if (accountMenu && dropdown && !accountMenu.contains(event.target)) {
dropdown.classList.remove('active');
}
});
......@@ -423,6 +464,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<h1>AISBF Dashboard</h1>
{% if request.session.logged_in %}
<div class="header-actions">
<a href="{{ url_for(request, '/dashboard/docs') }}" class="btn btn-secondary">Docs</a>
<a href="{{ url_for(request, '/dashboard/about') }}" class="btn btn-secondary">About</a>
<a href="{{ url_for(request, '/dashboard/license') }}" class="btn btn-secondary">License</a>
{% if request.session.role == 'admin' %}
<button onclick="restartServer()" class="btn btn-warning">Restart Server</button>
......@@ -433,6 +476,22 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</div>
</div>
{% if request.session.logged_in and show_upgrade_banner %}
<div class="upgrade-banner">
<div class="container">
<div class="d-flex align-items-center justify-content-between py-2">
<div>
<i class="fas fa-rocket me-2"></i>
<span>Want higher limits? <strong>Upgrade your plan</strong> for more requests and features!</span>
</div>
<a href="{{ url_for(request, '/dashboard/pricing') }}" class="btn btn-sm btn-light">
View Pricing Plans <i class="fas fa-arrow-right ms-1"></i>
</a>
</div>
</div>
</div>
{% endif %}
{% if request.session.logged_in %}
<div class="container">
<div class="nav">
......@@ -445,9 +504,11 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
{% 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/settings') }}" {% if '/settings' in request.path %}class="active"{% endif %}>Settings</a>
<a href="{{ url_for(request, '/dashboard/admin/tiers') }}" {% if '/admin/tiers' in request.path %}class="active"{% endif %}>Tiers</a>
{% endif %}
{% if show_upgrade_button %}
<a href="{{ url_for(request, '/dashboard/pricing') }}" class="upgrade-button rainbow-text">✨ Upgrade! ✨</a>
{% endif %}
<a href="{{ url_for(request, '/dashboard/docs') }}" {% if '/docs' in request.path %}class="active"{% endif %}>Docs</a>
<a href="{{ url_for(request, '/dashboard/about') }}" {% if '/about' in request.path %}class="active"{% endif %}>About</a>
{% if request.session.user_id %}
<div class="account-menu">
<div class="account-trigger" onclick="toggleAccountMenu()">
......@@ -458,6 +519,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
{% if request.session.user_id %}
{% if request.session.user_id %}
<a href="{{ url_for(request, '/dashboard/profile') }}">Edit Profile</a>
<a href="{{ url_for(request, '/dashboard/subscription') }}">Subscription</a>
<a href="{{ url_for(request, '/dashboard/billing') }}">Billing</a>
<a href="{{ url_for(request, '/dashboard/change-password') }}">Change Password</a>
{% endif %}
{% endif %}
......@@ -473,5 +536,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
{% block content %}{% endblock %}
</div>
</div>
{% block extra_js %}{% endblock %}
</body>
</html>
{% extends "base.html" %}
{% block title %}{{ 'Edit' if tier else 'Create' }} Tier{% endblock %}
{% block content %}
<div style="max-width: 800px; margin: 0 auto;">
<h2 style="margin-bottom: 30px;">
<i class="fas fa-layer-group" style="color: #4a9eff;"></i>
{{ 'Edit Tier' if tier else 'Create New Tier' }}
</h2>
<form method="POST" action="{{ url_for(request, '/dashboard/admin/tiers/save') }}">
<input type="hidden" name="tier_id" value="{{ tier.id if tier else '' }}">
<div style="background: #16213e; border: 2px solid #4a9eff; border-radius: 8px; padding: 20px; margin-bottom: 20px;">
<div style="margin-bottom: 20px;">
<label style="display: block; margin-bottom: 5px; font-weight: 500; color: #e0e0e0; font-size: 16px;">Tier Name</label>
<input type="text" name="name" style="width: 100%; padding: 12px; border: 1px solid #0f3460; border-radius: 5px; background: #1a1a2e; color: #e0e0e0; font-size: 16px;" required placeholder="e.g. Professional Tier" value="{{ tier.name if tier else '' }}">
</div>
<div style="margin-bottom: 20px;">
<label style="display: block; margin-bottom: 5px; font-weight: 500; color: #e0e0e0; font-size: 16px;">Description</label>
<textarea name="description" style="width: 100%; padding: 12px; border: 1px solid #0f3460; border-radius: 5px; background: #1a1a2e; color: #e0e0e0; font-size: 16px; min-height: 80px;" placeholder="Tier description">{{ tier.description if tier else '' }}</textarea>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 15px; margin-bottom: 20px;">
<div style="background: #1a1a2e; border: 1px solid #0f3460; border-radius: 8px; padding: 15px;">
<label style="display: block; margin-bottom: 8px; font-weight: 500; color: #4a9eff; font-size: 14px;">
<i class="fas fa-calendar-day me-2"></i> Monthly Price
</label>
<div style="display: flex; align-items: center;">
<span style="background: #0f3460; color: #e0e0e0; padding: 12px; border-radius: 5px 0 0 5px; border: 1px solid #0f3460;">$</span>
<input type="number" name="price_monthly" style="flex: 1; padding: 12px; border: 1px solid #0f3460; border-left: none; border-radius: 0 5px 5px 0; background: #16213e; color: #e0e0e0; font-size: 16px;" step="0.01" min="0" value="{{ tier.price_monthly if tier else '0.00' }}">
</div>
</div>
<div style="background: #1a1a2e; border: 1px solid #0f3460; border-radius: 8px; padding: 15px;">
<label style="display: block; margin-bottom: 8px; font-weight: 500; color: #4ade80; font-size: 14px;">
<i class="fas fa-calendar-alt me-2"></i> Yearly Price
</label>
<div style="display: flex; align-items: center;">
<span style="background: #0f3460; color: #e0e0e0; padding: 12px; border-radius: 5px 0 0 5px; border: 1px solid #0f3460;">$</span>
<input type="number" name="price_yearly" style="flex: 1; padding: 12px; border: 1px solid #0f3460; border-left: none; border-radius: 0 5px 5px 0; background: #16213e; color: #e0e0e0; font-size: 16px;" step="0.01" min="0" value="{{ tier.price_yearly if tier else '0.00' }}">
</div>
</div>
</div>
<div style="background: #1a1a2e; border: 1px solid #0f3460; border-radius: 8px; margin-bottom: 20px; overflow: hidden;">
<div style="padding: 15px; border-bottom: 1px solid #0f3460;">
<h6 style="margin: 0; color: #e0e0e0; font-weight: 500;"><i class="fas fa-sliders-h me-2"></i> Usage Limits</h6>
<p style="margin: 5px 0 0 0; color: #a0a0a0; font-size: 12px;">Use <code style="background: #0f3460; padding: 2px 4px; border-radius: 3px;">-1</code> for unlimited, <code style="background: #0f3460; padding: 2px 4px; border-radius: 3px;">0</code> to disable, or any positive number for the limit</p>
</div>
<div style="padding: 15px;">
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px;">
<div>
<label style="display: block; margin-bottom: 5px; color: #a0a0a0; font-size: 12px;">Max Requests per Day</label>
<input type="number" name="max_requests_per_day" style="width: 100%; padding: 12px; border: 1px solid #0f3460; border-radius: 5px; background: #16213e; color: #e0e0e0; font-size: 14px;" min="-1" value="{{ tier.max_requests_per_day if tier else '-1' }}">
</div>
<div>
<label style="display: block; margin-bottom: 5px; color: #a0a0a0; font-size: 12px;">Max Requests per Month</label>
<input type="number" name="max_requests_per_month" style="width: 100%; padding: 12px; border: 1px solid #0f3460; border-radius: 5px; background: #16213e; color: #e0e0e0; font-size: 14px;" min="-1" value="{{ tier.max_requests_per_month if tier else '-1' }}">
</div>
<div>
<label style="display: block; margin-bottom: 5px; color: #a0a0a0; font-size: 12px;">Max Providers</label>
<input type="number" name="max_providers" style="width: 100%; padding: 12px; border: 1px solid #0f3460; border-radius: 5px; background: #16213e; color: #e0e0e0; font-size: 14px;" min="-1" value="{{ tier.max_providers if tier else '-1' }}">
</div>
<div>
<label style="display: block; margin-bottom: 5px; color: #a0a0a0; font-size: 12px;">Max Rotations</label>
<input type="number" name="max_rotations" style="width: 100%; padding: 12px; border: 1px solid #0f3460; border-radius: 5px; background: #16213e; color: #e0e0e0; font-size: 14px;" min="-1" value="{{ tier.max_rotations if tier else '-1' }}">
</div>
<div>
<label style="display: block; margin-bottom: 5px; color: #a0a0a0; font-size: 12px;">Max Autoselections</label>
<input type="number" name="max_autoselections" style="width: 100%; padding: 12px; border: 1px solid #0f3460; border-radius: 5px; background: #16213e; color: #e0e0e0; font-size: 14px;" min="-1" value="{{ tier.max_autoselections if tier else '-1' }}">
</div>
<div>
<label style="display: block; margin-bottom: 5px; color: #a0a0a0; font-size: 12px;">Max Models per Rotation</label>
<input type="number" name="max_rotation_models" style="width: 100%; padding: 12px; border: 1px solid #0f3460; border-radius: 5px; background: #16213e; color: #e0e0e0; font-size: 14px;" min="-1" value="{{ tier.max_rotation_models if tier else '-1' }}">
</div>
<div>
<label style="display: block; margin-bottom: 5px; color: #a0a0a0; font-size: 12px;">Max Models per Autoselection</label>
<input type="number" name="max_autoselection_models" style="width: 100%; padding: 12px; border: 1px solid #0f3460; border-radius: 5px; background: #16213e; color: #e0e0e0; font-size: 14px;" min="-1" value="{{ tier.max_autoselection_models if tier else '-1' }}">
</div>
</div>
</div>
</div>
<div style="margin-bottom: 15px;">
<label style="cursor: pointer; font-weight: 500; color: #e0e0e0;">
<input type="checkbox" name="is_active" value="1" {{ 'checked' if not tier or tier.is_active else '' }} style="margin-right: 10px;">
Tier is active and available for users
</label>
</div>
</div>
<div style="display: flex; gap: 10px;">
<button type="submit" class="btn" style="background: #4a9eff; color: white;">
<i class="fas fa-save me-2"></i>Save Tier
</button>
<a href="{{ url_for(request, '/dashboard/admin/tiers') }}" class="btn" style="background: #6c757d; color: white;">
<i class="fas fa-times me-2"></i>Cancel
</a>
</div>
</form>
</div>
{% endblock %}
{% extends "base.html" %}
{% block title %}Account Tiers Management{% endblock %}
{% block content %}
<h2 style="margin-bottom: 30px;"><i class="fas fa-layer-group" style="color: #4a9eff;"></i> Account Tiers Management</h2>
<p style="color: #a0a0a0; margin-bottom: 30px;">Configure account tiers, pricing, and usage limits for your users</p>
<!-- Tier List -->
<div style="background: #16213e; border: 2px solid #4a9eff; border-radius: 8px; padding: 20px; margin-bottom: 20px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h3 style="margin: 0; color: #4a9eff;">
<i class="fas fa-list me-2"></i>Available Tiers
</h3>
<a href="{{ url_for(request, '/dashboard/admin/tiers/create') }}" class="btn" style="background: #4a9eff; color: white;">
<i class="fas fa-plus me-2"></i>Create New Tier
</a>
</div>
<div style="overflow-x: auto;">
<table style="width: 100%; border-collapse: collapse; margin-top: 20px;">
<thead>
<tr style="background: #0f3460; color: #e0e0e0;">
<th style="padding: 12px; text-align: center; border-bottom: 1px solid #0f3460; font-weight: 600;">ID</th>
<th style="padding: 12px; text-align: left; border-bottom: 1px solid #0f3460; font-weight: 600;">Tier Name</th>
<th style="padding: 12px; text-align: center; border-bottom: 1px solid #0f3460; font-weight: 600;">Price (Monthly)</th>
<th style="padding: 12px; text-align: center; border-bottom: 1px solid #0f3460; font-weight: 600;">Price (Yearly)</th>
<th style="padding: 12px; text-align: center; border-bottom: 1px solid #0f3460; font-weight: 600;">Max Requests / Day</th>
<th style="padding: 12px; text-align: center; border-bottom: 1px solid #0f3460; font-weight: 600;">Max Requests / Month</th>
<th style="padding: 12px; text-align: center; border-bottom: 1px solid #0f3460; font-weight: 600;">Max Providers</th>
<th style="padding: 12px; text-align: center; border-bottom: 1px solid #0f3460; font-weight: 600;">Max Rotations</th>
<th style="padding: 12px; text-align: center; border-bottom: 1px solid #0f3460; font-weight: 600;">Max Autoselections</th>
<th style="padding: 12px; text-align: center; border-bottom: 1px solid #0f3460; font-weight: 600;">Max Models / Rotation</th>
<th style="padding: 12px; text-align: center; border-bottom: 1px solid #0f3460; font-weight: 600;">Max Models / Autoselect</th>
<th style="padding: 12px; text-align: center; border-bottom: 1px solid #0f3460; font-weight: 600;">Status</th>
<th style="padding: 12px; text-align: center; border-bottom: 1px solid #0f3460; font-weight: 600;">Actions</th>
</tr>
</thead>
<tbody id="tiersTableBody" style="color: #e0e0e0;">
{% for tier in tiers %}
<tr class="tier-row">
<td>{{ tier.id }}</td>
<td><strong>{{ tier.name }}{% if tier.is_default %} <span class="badge bg-success">Default</span>{% endif %}</strong></td>
<td>{{ tier.price_monthly }}</td>
<td>{{ tier.price_yearly }}</td>
<td>{% if tier.max_requests_per_day == -1 %}Unlimited{% elif tier.max_requests_per_day == 0 %}Blocked{% else %}{{ tier.max_requests_per_day }}{% endif %}</td>
<td>{% if tier.max_requests_per_month == -1 %}Unlimited{% elif tier.max_requests_per_month == 0 %}Blocked{% else %}{{ tier.max_requests_per_month }}{% endif %}</td>
<td>{% if tier.max_providers == -1 %}Unlimited{% elif tier.max_providers == 0 %}Blocked{% else %}{{ tier.max_providers }}{% endif %}</td>
<td>{% if tier.max_rotations == -1 %}Unlimited{% elif tier.max_rotations == 0 %}Blocked{% else %}{{ tier.max_rotations }}{% endif %}</td>
<td>{% if tier.max_autoselections == -1 %}Unlimited{% elif tier.max_autoselections == 0 %}Blocked{% else %}{{ tier.max_autoselections }}{% endif %}</td>
<td>{% if tier.max_rotation_models == -1 %}Unlimited{% elif tier.max_rotation_models == 0 %}Blocked{% else %}{{ tier.max_rotation_models }}{% endif %}</td>
<td>{% if tier.max_autoselection_models == -1 %}Unlimited{% elif tier.max_autoselection_models == 0 %}Blocked{% else %}{{ tier.max_autoselection_models }}{% endif %}</td>
<td>{% if tier.is_active %}<span class="badge bg-success">Active</span>{% else %}<span class="badge bg-secondary">Inactive</span>{% endif %}</td>
<td style="white-space: nowrap;">
<a href="{{ url_for(request, '/dashboard/admin/tiers/edit') }}/{{ tier.id }}" class="btn btn-sm btn-outline-primary me-1" title="Edit this tier">
Edit
</a>
{% if not tier.is_default %}
<button type="button" class="btn btn-sm btn-outline-danger" onclick="deleteTier({{ tier.id }})" title="Delete this tier">
Delete
</button>
{% else %}
<span style="font-size: 11px; color: #888;">(default)</span>
{% endif %}
</td>
</tr>
{% endfor %}
</table>
</div>
</div>
<!-- Global Currency Settings -->
<div style="background: #16213e; border: 2px solid #17a2b8; border-radius: 8px; padding: 20px; margin-bottom: 20px;">
<h3 style="margin: 0 0 20px 0; color: #17a2b8;">
<i class="fas fa-dollar-sign me-2"></i>Global Currency Settings
</h3>
<form id="currencySettingsForm">
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 15px; margin-bottom: 20px;">
<div>
<label style="display: block; margin-bottom: 5px; font-weight: 500; color: #e0e0e0;">Currency Code</label>
<select style="width: 100%; padding: 8px; border: 1px solid #0f3460; border-radius: 3px; font-size: 14px; background: #1a1a2e; color: #e0e0e0;" id="currencyCode">
<option value="USD">USD - US Dollar</option>
<option value="EUR">EUR - Euro</option>
<option value="GBP">GBP - British Pound</option>
<option value="JPY">JPY - Japanese Yen</option>
<option value="CAD">CAD - Canadian Dollar</option>
<option value="AUD">AUD - Australian Dollar</option>
<option value="CHF">CHF - Swiss Franc</option>
<option value="CNY">CNY - Chinese Yuan</option>
<option value="INR">INR - Indian Rupee</option>
<option value="BRL">BRL - Brazilian Real</option>
</select>
</div>
<div>
<label style="display: block; margin-bottom: 5px; font-weight: 500; color: #e0e0e0;">Currency Symbol</label>
<input type="text" style="width: 100%; padding: 8px; border: 1px solid #0f3460; border-radius: 3px; font-size: 14px; background: #1a1a2e; color: #e0e0e0;" id="currencySymbol" value="$">
</div>
<div>
<label style="display: block; margin-bottom: 5px; font-weight: 500; color: #e0e0e0;">Decimal Places</label>
<select style="width: 100%; padding: 8px; border: 1px solid #0f3460; border-radius: 3px; font-size: 14px; background: #1a1a2e; color: #e0e0e0;" id="currencyDecimals">
<option value="0">0</option>
<option value="1">1</option>
<option value="2" selected>2</option>
<option value="3">3</option>
</select>
</div>
</div>
<button type="submit" class="btn" style="background: #17a2b8; color: white;">
<i class="fas fa-save me-2"></i>Save Currency Settings
</button>
</form>
</div>
<!-- Payment Gateways Configuration -->
<div style="background: #16213e; border: 2px solid #f39c12; border-radius: 8px; padding: 20px; margin-bottom: 20px;">
<h3 style="margin: 0 0 20px 0; color: #f39c12;">
<i class="fas fa-credit-card me-2"></i>Payment Gateways Configuration
</h3>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 20px;">
<!-- PayPal -->
<div style="background: #1a1a2e; border: 1px solid #0f3460; border-radius: 8px; overflow: hidden;">
<div style="background: #0070ba; color: white; padding: 15px; display: flex; justify-content: space-between; align-items: center;">
<h6 style="margin: 0;"><i class="fab fa-paypal me-2"></i>PayPal</h6>
<label style="margin: 0; cursor: pointer;">
<input type="checkbox" id="paypalEnabled" style="margin-right: 5px;">Enabled
</label>
</div>
<div style="padding: 15px;">
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px; font-weight: 500; color: #e0e0e0;">Client ID</label>
<input type="text" style="width: 100%; padding: 8px; border: 1px solid #0f3460; border-radius: 3px; background: #16213e; color: #e0e0e0;" id="paypalClientId" placeholder="PayPal Client ID">
</div>
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px; font-weight: 500; color: #e0e0e0;">Client Secret</label>
<input type="password" style="width: 100%; padding: 8px; border: 1px solid #0f3460; border-radius: 3px; background: #16213e; color: #e0e0e0;" id="paypalClientSecret" placeholder="PayPal Client Secret">
</div>
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px; font-weight: 500; color: #e0e0e0;">Webhook Secret</label>
<input type="password" style="width: 100%; padding: 8px; border: 1px solid #0f3460; border-radius: 3px; background: #16213e; color: #e0e0e0;" id="paypalWebhookSecret" placeholder="Webhook Secret">
</div>
<label style="cursor: pointer;">
<input type="checkbox" id="paypalSandbox" checked style="margin-right: 5px;">Sandbox Mode
</label>
</div>
</div>
<!-- Stripe -->
<div style="background: #1a1a2e; border: 1px solid #0f3460; border-radius: 8px; overflow: hidden;">
<div style="background: #635bff; color: white; padding: 15px; display: flex; justify-content: space-between; align-items: center;">
<h6 style="margin: 0;"><i class="fab fa-stripe me-2"></i>Stripe</h6>
<label style="margin: 0; cursor: pointer;">
<input type="checkbox" id="stripeEnabled" style="margin-right: 5px;">Enabled
</label>
</div>
<div style="padding: 15px;">
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px; font-weight: 500; color: #e0e0e0;">Publishable Key</label>
<input type="text" style="width: 100%; padding: 8px; border: 1px solid #0f3460; border-radius: 3px; background: #16213e; color: #e0e0e0;" id="stripePublishableKey" placeholder="pk_...">
</div>
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px; font-weight: 500; color: #e0e0e0;">Secret Key</label>
<input type="password" style="width: 100%; padding: 8px; border: 1px solid #0f3460; border-radius: 3px; background: #16213e; color: #e0e0e0;" id="stripeSecretKey" placeholder="sk_...">
</div>
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px; font-weight: 500; color: #e0e0e0;">Webhook Secret</label>
<input type="password" style="width: 100%; padding: 8px; border: 1px solid #0f3460; border-radius: 3px; background: #16213e; color: #e0e0e0;" id="stripeWebhookSecret" placeholder="whsec_...">
</div>
<label style="cursor: pointer;">
<input type="checkbox" id="stripeSandbox" checked style="margin-right: 5px;">Test Mode
</label>
</div>
</div>
<!-- Bitcoin -->
<div style="background: #1a1a2e; border: 1px solid #0f3460; border-radius: 8px; overflow: hidden;">
<div style="background: #f7931a; color: white; padding: 15px; display: flex; justify-content: space-between; align-items: center;">
<h6 style="margin: 0;"><i class="fab fa-bitcoin me-2"></i>Bitcoin</h6>
<label style="margin: 0; cursor: pointer;">
<input type="checkbox" id="bitcoinEnabled" style="margin-right: 5px;">Enabled
</label>
</div>
<div style="padding: 15px;">
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px; font-weight: 500; color: #e0e0e0;">Bitcoin Address</label>
<input type="text" style="width: 100%; padding: 8px; border: 1px solid #0f3460; border-radius: 3px; background: #16213e; color: #e0e0e0;" id="bitcoinAddress" placeholder="bc1q...">
</div>
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px; font-weight: 500; color: #e0e0e0;">Minimum Confirmations</label>
<input type="number" style="width: 100%; padding: 8px; border: 1px solid #0f3460; border-radius: 3px; background: #16213e; color: #e0e0e0;" id="bitcoinConfirmations" value="3" min="1" max="10">
</div>
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px; font-weight: 500; color: #e0e0e0;">Expiration Time (minutes)</label>
<input type="number" style="width: 100%; padding: 8px; border: 1px solid #0f3460; border-radius: 3px; background: #16213e; color: #e0e0e0;" id="bitcoinExpiration" value="120" min="15" max="1440">
</div>
</div>
</div>
<!-- Ethereum -->
<div style="background: #1a1a2e; border: 1px solid #0f3460; border-radius: 8px; overflow: hidden;">
<div style="background: #627eea; color: white; padding: 15px; display: flex; justify-content: space-between; align-items: center;">
<h6 style="margin: 0;"><i class="fab fa-ethereum me-2"></i>Ethereum (ETH)</h6>
<label style="margin: 0; cursor: pointer;">
<input type="checkbox" id="ethEnabled" style="margin-right: 5px;">Enabled
</label>
</div>
<div style="padding: 15px;">
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px; font-weight: 500; color: #e0e0e0;">ETH Wallet Address</label>
<input type="text" style="width: 100%; padding: 8px; border: 1px solid #0f3460; border-radius: 3px; background: #16213e; color: #e0e0e0;" id="ethAddress" placeholder="0x...">
</div>
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px; font-weight: 500; color: #e0e0e0;">Minimum Confirmations</label>
<input type="number" style="width: 100%; padding: 8px; border: 1px solid #0f3460; border-radius: 3px; background: #16213e; color: #e0e0e0;" id="ethConfirmations" value="12" min="1" max="50">
</div>
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px; font-weight: 500; color: #e0e0e0;">Chain ID</label>
<select style="width: 100%; padding: 8px; border: 1px solid #0f3460; border-radius: 3px; background: #16213e; color: #e0e0e0;" id="ethChainId">
<option value="1">Ethereum Mainnet</option>
<option value="5">Goerli Testnet</option>
<option value="11155111">Sepolia Testnet</option>
</select>
</div>
</div>
</div>
<!-- USDT -->
<div style="background: #1a1a2e; border: 1px solid #0f3460; border-radius: 8px; overflow: hidden;">
<div style="background: #26a17b; color: white; padding: 15px; display: flex; justify-content: space-between; align-items: center;">
<h6 style="margin: 0;"><i class="fas fa-dollar-sign me-2"></i>Tether (USDT)</h6>
<label style="margin: 0; cursor: pointer;">
<input type="checkbox" id="usdtEnabled" style="margin-right: 5px;">Enabled
</label>
</div>
<div style="padding: 15px;">
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px; font-weight: 500; color: #e0e0e0;">USDT Wallet Address</label>
<input type="text" style="width: 100%; padding: 8px; border: 1px solid #0f3460; border-radius: 3px; background: #16213e; color: #e0e0e0;" id="usdtAddress" placeholder="0x...">
</div>
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px; font-weight: 500; color: #e0e0e0;">Network</label>
<select style="width: 100%; padding: 8px; border: 1px solid #0f3460; border-radius: 3px; background: #16213e; color: #e0e0e0;" id="usdtNetwork">
<option value="erc20">ERC20 (Ethereum)</option>
<option value="trc20">TRC20 (Tron)</option>
<option value="bep20">BEP20 (BSC)</option>
<option value="solana">Solana</option>
</select>
</div>
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px; font-weight: 500; color: #e0e0e0;">Minimum Confirmations</label>
<input type="number" style="width: 100%; padding: 8px; border: 1px solid #0f3460; border-radius: 3px; background: #16213e; color: #e0e0e0;" id="usdtConfirmations" value="3" min="1" max="50">
</div>
</div>
</div>
<!-- USDC -->
<div style="background: #1a1a2e; border: 1px solid #0f3460; border-radius: 8px; overflow: hidden;">
<div style="background: #2775ca; color: white; padding: 15px; display: flex; justify-content: space-between; align-items: center;">
<h6 style="margin: 0;"><i class="fas fa-coins me-2"></i>USD Coin (USDC)</h6>
<label style="margin: 0; cursor: pointer;">
<input type="checkbox" id="usdcEnabled" style="margin-right: 5px;">Enabled
</label>
</div>
<div style="padding: 15px;">
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px; font-weight: 500; color: #e0e0e0;">USDC Wallet Address</label>
<input type="text" style="width: 100%; padding: 8px; border: 1px solid #0f3460; border-radius: 3px; background: #16213e; color: #e0e0e0;" id="usdcAddress" placeholder="0x...">
</div>
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px; font-weight: 500; color: #e0e0e0;">Network</label>
<select style="width: 100%; padding: 8px; border: 1px solid #0f3460; border-radius: 3px; background: #16213e; color: #e0e0e0;" id="usdcNetwork">
<option value="erc20">ERC20 (Ethereum)</option>
<option value="solana">Solana</option>
<option value="bep20">BEP20 (BSC)</option>
<option value="algorand">Algorand</option>
</select>
</div>
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px; font-weight: 500; color: #e0e0e0;">Minimum Confirmations</label>
<input type="number" style="width: 100%; padding: 8px; border: 1px solid #0f3460; border-radius: 3px; background: #16213e; color: #e0e0e0;" id="usdcConfirmations" value="3" min="1" max="50">
</div>
</div>
</div>
</div>
<button type="button" class="btn" onclick="savePaymentGateways()" style="background: #f39c12; color: white; margin-top: 20px;">
<i class="fas fa-save me-2"></i>Save Payment Gateway Configuration
</button>
</div>
<!-- Create/Edit Tier Modal -->
<div class="modal fade" id="tierModal" tabindex="-1" aria-labelledby="tierModalLabel" aria-hidden="true" style="display: none;">
<div class="modal-dialog modal-lg" style="max-width: 90%; max-height: 90vh; margin: 2rem auto; overflow-y: auto;">
<div class="modal-content" style="background: #16213e; color: #e0e0e0; border: 2px solid #4a9eff; max-height: 85vh; overflow-y: auto;">
<div class="modal-header" style="background: #0f3460; border-bottom: 1px solid #4a9eff; display: flex; justify-content: space-between; align-items: center;">
<h5 class="modal-title" id="tierModalLabel" style="color: #4a9eff; margin: 0;">Create New Tier</h5>
<button type="button" class="btn-close btn-close-white" onclick="closeTierModal()" aria-label="Close"></button>
</div>
<div class="modal-body" style="background: #16213e;">
<form id="tierForm">
<input type="hidden" id="tierId" value="">
<div style="margin-bottom: 20px;">
<label style="display: block; margin-bottom: 5px; font-weight: 500; color: #e0e0e0; font-size: 16px;">Tier Name</label>
<input type="text" style="width: 100%; padding: 12px; border: 1px solid #0f3460; border-radius: 5px; background: #1a1a2e; color: #e0e0e0; font-size: 16px;" id="tierName" required placeholder="e.g. Professional Tier">
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 15px; margin-bottom: 20px;">
<div style="background: #1a1a2e; border: 1px solid #0f3460; border-radius: 8px; padding: 15px;">
<div style="margin-bottom: 0;">
<label style="display: block; margin-bottom: 8px; font-weight: 500; color: #4a9eff; font-size: 14px;">
<i class="fas fa-calendar-day me-2"></i> Monthly Price
</label>
<div style="display: flex; align-items: center;">
<span style="background: #0f3460; color: #e0e0e0; padding: 12px; border-radius: 5px 0 0 5px; border: 1px solid #0f3460;">$</span>
<input type="number" style="flex: 1; padding: 12px; border: 1px solid #0f3460; border-left: none; border-radius: 0 5px 5px 0; background: #16213e; color: #e0e0e0; font-size: 16px;" id="tierPriceMonthly" step="0.01" min="0" value="0.00">
</div>
</div>
</div>
<div style="background: #1a1a2e; border: 1px solid #0f3460; border-radius: 8px; padding: 15px;">
<div style="margin-bottom: 0;">
<label style="display: block; margin-bottom: 8px; font-weight: 500; color: #4ade80; font-size: 14px;">
<i class="fas fa-calendar-alt me-2"></i> Yearly Price
</label>
<div style="display: flex; align-items: center;">
<span style="background: #0f3460; color: #e0e0e0; padding: 12px; border-radius: 5px 0 0 5px; border: 1px solid #0f3460;">$</span>
<input type="number" style="flex: 1; padding: 12px; border: 1px solid #0f3460; border-left: none; border-radius: 0 5px 5px 0; background: #16213e; color: #e0e0e0; font-size: 16px;" id="tierPriceYearly" step="0.01" min="0" value="0.00">
</div>
</div>
</div>
</div>
<div style="background: #1a1a2e; border: 1px solid #0f3460; border-radius: 8px; margin-bottom: 20px; overflow: hidden;">
<div style="padding: 15px; border-bottom: 1px solid #0f3460;">
<h6 style="margin: 0; color: #e0e0e0; font-weight: 500;"><i class="fas fa-sliders-h me-2"></i> Usage Limits</h6>
<p style="margin: 5px 0 0 0; color: #a0a0a0; font-size: 12px;">Use <code style="background: #0f3460; padding: 2px 4px; border-radius: 3px;">-1</code> for unlimited, <code style="background: #0f3460; padding: 2px 4px; border-radius: 3px;">0</code> to disable, or any positive number for the limit</p>
</div>
<div style="padding: 15px;">
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px;">
<div style="position: relative;">
<input type="number" style="width: 100%; padding: 12px 12px 12px 0; border: 1px solid #0f3460; border-radius: 5px; background: #16213e; color: #e0e0e0; font-size: 14px;" id="tierMaxRequestsDay" min="-1" value="-1">
<label style="position: absolute; top: 12px; left: 0; color: #a0a0a0; font-size: 12px; pointer-events: none; transition: all 0.2s;">Max Requests per Day</label>
</div>
<div style="position: relative;">
<input type="number" style="width: 100%; padding: 12px 12px 12px 0; border: 1px solid #0f3460; border-radius: 5px; background: #16213e; color: #e0e0e0; font-size: 14px;" id="tierMaxRequestsMonth" min="-1" value="-1">
<label style="position: absolute; top: 12px; left: 0; color: #a0a0a0; font-size: 12px; pointer-events: none; transition: all 0.2s;">Max Requests per Month</label>
</div>
<div style="position: relative;">
<input type="number" style="width: 100%; padding: 12px 12px 12px 0; border: 1px solid #0f3460; border-radius: 5px; background: #16213e; color: #e0e0e0; font-size: 14px;" id="tierMaxProviders" min="-1" value="-1">
<label style="position: absolute; top: 12px; left: 0; color: #a0a0a0; font-size: 12px; pointer-events: none; transition: all 0.2s;">Max Providers</label>
</div>
<div style="position: relative;">
<input type="number" style="width: 100%; padding: 12px 12px 12px 0; border: 1px solid #0f3460; border-radius: 5px; background: #16213e; color: #e0e0e0; font-size: 14px;" id="tierMaxRotations" min="-1" value="-1">
<label style="position: absolute; top: 12px; left: 0; color: #a0a0a0; font-size: 12px; pointer-events: none; transition: all 0.2s;">Max Rotations</label>
</div>
<div style="position: relative;">
<input type="number" style="width: 100%; padding: 12px 12px 12px 0; border: 1px solid #0f3460; border-radius: 5px; background: #16213e; color: #e0e0e0; font-size: 14px;" id="tierMaxAutoselections" min="-1" value="-1">
<label style="position: absolute; top: 12px; left: 0; color: #a0a0a0; font-size: 12px; pointer-events: none; transition: all 0.2s;">Max Autoselections</label>
</div>
<div style="position: relative;">
<input type="number" style="width: 100%; padding: 12px 12px 12px 0; border: 1px solid #0f3460; border-radius: 5px; background: #16213e; color: #e0e0e0; font-size: 14px;" id="tierMaxRotationModels" min="-1" value="-1">
<label style="position: absolute; top: 12px; left: 0; color: #a0a0a0; font-size: 12px; pointer-events: none; transition: all 0.2s;">Max Models per Rotation</label>
</div>
<div style="position: relative; grid-column: 1 / -1;">
<input type="number" style="width: 100%; padding: 12px 12px 12px 0; border: 1px solid #0f3460; border-radius: 5px; background: #16213e; color: #e0e0e0; font-size: 14px;" id="tierMaxAutoselectionModels" min="-1" value="-1">
<label style="position: absolute; top: 12px; left: 0; color: #a0a0a0; font-size: 12px; pointer-events: none; transition: all 0.2s;">Max Models per Autoselection</label>
</div>
</div>
</div>
</div>
<div style="margin-bottom: 15px;">
<label style="cursor: pointer; font-weight: 500; color: #e0e0e0;">
<input type="checkbox" id="tierIsActive" checked style="margin-right: 10px;">
Tier is active and available for users
</label>
</div>
</form>
</div>
<div class="modal-footer" style="background: #0f3460; border-top: 1px solid #4a9eff;">
<button type="button" class="btn" onclick="closeTierModal()" style="background: #6c757d; color: white;">Cancel</button>
<button type="button" class="btn" onclick="saveTier()" style="background: #4a9eff; color: white;">Save Tier</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_css %}
<style>
.tier-row {
border-bottom: 1px solid #0f3460;
color: #e0e0e0;
}
.tier-row:hover {
background: #1a1a2e;
}
</style>
{% endblock %}
{% block extra_js %}
<script>
document.addEventListener('DOMContentLoaded', function() {
try {
loadCurrencySettings();
loadPaymentGateways();
} catch (error) {
console.error('Error initializing page:', error);
}
});
function loadTiers() {
// Tiers are already loaded via server-side rendering
// This function kept for refresh after edits
const tbody = document.getElementById('tiersTableBody');
if (!tbody) return;
const rows = tbody.querySelectorAll('tr');
if (rows.length > 0) {
// Data already loaded
return;
}
// If no data, fetch from API (fallback)
fetch('{{ url_for(request, "/api/admin/tiers") }}')
.then(response => response.json())
.then(tiers => {
renderTiers(tiers);
})
.catch(error => {
console.error('Error loading tiers:', error);
});
}
function renderTiers(tiers) {
const tbody = document.getElementById('tiersTableBody');
if (!tbody) return;
tbody.innerHTML = '';
tiers.forEach(tier => {
const tr = document.createElement('tr');
tr.className = 'tier-row';
tr.innerHTML = `
<td>${tier.id}</td>
<td><strong>${tier.name}</strong>${tier.is_default ? ' <span class="badge bg-success">Default</span>' : ''}</td>
<td>${tier.price_monthly}</td>
<td>${tier.price_yearly}</td>
<td>${tier.max_requests_per_day === -1 ? 'Unlimited' : tier.max_requests_per_day === 0 ? 'Blocked' : tier.max_requests_per_day}</td>
<td>${tier.max_requests_per_month === -1 ? 'Unlimited' : tier.max_requests_per_month === 0 ? 'Blocked' : tier.max_requests_per_month}</td>
<td>${tier.max_providers === -1 ? 'Unlimited' : tier.max_providers === 0 ? 'Blocked' : tier.max_providers}</td>
<td>${tier.max_rotations === -1 ? 'Unlimited' : tier.max_rotations === 0 ? 'Blocked' : tier.max_rotations}</td>
<td>${tier.max_autoselections === -1 ? 'Unlimited' : tier.max_autoselections === 0 ? 'Blocked' : tier.max_autoselections}</td>
<td>${tier.max_rotation_models === -1 ? 'Unlimited' : tier.max_rotation_models === 0 ? 'Blocked' : tier.max_rotation_models}</td>
<td>${tier.max_autoselection_models === -1 ? 'Unlimited' : tier.max_autoselection_models === 0 ? 'Blocked' : tier.max_autoselection_models}</td>
<td>${tier.is_active ? '<span class="badge bg-success">Active</span>' : '<span class="badge bg-secondary">Inactive</span>'}</td>
<td>
<button class="btn btn-sm btn-outline-primary me-1" onclick="editTier(${tier.id})">
<i class="fas fa-edit"></i>
</button>
${!tier.is_default ? `
<button class="btn btn-sm btn-outline-danger" onclick="deleteTier(${tier.id})">
<i class="fas fa-trash"></i>
</button>
` : ''}
</td>
`;
tbody.appendChild(tr);
});
}
function showCreateTierModal() {
document.getElementById('tierModalLabel').textContent = 'Create New Tier';
document.getElementById('tierId').value = '';
document.getElementById('tierForm').reset();
document.getElementById('tierMaxRequestsDay').value = -1;
document.getElementById('tierMaxRequestsMonth').value = -1;
document.getElementById('tierMaxProviders').value = -1;
document.getElementById('tierMaxRotations').value = -1;
document.getElementById('tierMaxAutoselections').value = -1;
document.getElementById('tierMaxRotationModels').value = -1;
document.getElementById('tierMaxAutoselectionModels').value = -1;
document.getElementById('tierIsActive').checked = true;
// Show modal using custom CSS
const modal = document.getElementById('tierModal');
modal.style.display = 'flex';
modal.style.position = 'fixed';
modal.style.zIndex = '9999';
modal.style.top = '0';
modal.style.left = '0';
modal.style.width = '100%';
modal.style.height = '100%';
modal.style.backgroundColor = 'rgba(0,0,0,0.5)';
modal.style.alignItems = 'center';
modal.style.justifyContent = 'center';
modal.style.overflow = 'auto';
}
function editTier(tierId) {
// Fetch tier data from API
fetch('{{ url_for(request, "/api/admin/tiers") }}/' + tierId)
.then(response => response.json())
.then(tier => {
document.getElementById('tierModalLabel').textContent = 'Edit Tier';
document.getElementById('tierId').value = tier.id;
document.getElementById('tierName').value = tier.name;
document.getElementById('tierPriceMonthly').value = tier.price_monthly;
document.getElementById('tierPriceYearly').value = tier.price_yearly;
document.getElementById('tierMaxRequestsDay').value = tier.max_requests_per_day;
document.getElementById('tierMaxRequestsMonth').value = tier.max_requests_per_month;
document.getElementById('tierMaxProviders').value = tier.max_providers;
document.getElementById('tierMaxRotations').value = tier.max_rotations;
document.getElementById('tierMaxAutoselections').value = tier.max_autoselections;
document.getElementById('tierMaxRotationModels').value = tier.max_rotation_models;
document.getElementById('tierMaxAutoselectionModels').value = tier.max_autoselection_models;
document.getElementById('tierIsActive').checked = tier.is_active;
// Show modal using custom CSS
const modal = document.getElementById('tierModal');
modal.style.display = 'flex';
modal.style.position = 'fixed';
modal.style.zIndex = '9999';
modal.style.top = '0';
modal.style.left = '0';
modal.style.width = '100%';
modal.style.height = '100%';
modal.style.backgroundColor = 'rgba(0,0,0,0.5)';
modal.style.alignItems = 'center';
modal.style.justifyContent = 'center';
modal.style.overflow = 'auto';
})
.catch(error => {
showToast('Error loading tier: ' + error.message, 'danger');
});
}
function saveTier() {
const tierId = document.getElementById('tierId').value;
const url = tierId ? '{{ url_for(request, "/api/admin/tiers") }}/' + tierId : '{{ url_for(request, "/api/admin/tiers") }}';
const method = tierId ? 'PUT' : 'POST';
const tierData = {
name: document.getElementById('tierName').value,
price_monthly: parseFloat(document.getElementById('tierPriceMonthly').value),
price_yearly: parseFloat(document.getElementById('tierPriceYearly').value),
max_requests_per_day: parseInt(document.getElementById('tierMaxRequestsDay').value),
max_requests_per_month: parseInt(document.getElementById('tierMaxRequestsMonth').value),
max_providers: parseInt(document.getElementById('tierMaxProviders').value),
max_rotations: parseInt(document.getElementById('tierMaxRotations').value),
max_autoselections: parseInt(document.getElementById('tierMaxAutoselections').value),
max_rotation_models: parseInt(document.getElementById('tierMaxRotationModels').value),
max_autoselection_models: parseInt(document.getElementById('tierMaxAutoselectionModels').value),
is_active: document.getElementById('tierIsActive').checked
};
fetch(url, {
method: method,
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(tierData)
})
.then(response => response.json())
.then(result => {
// Hide modal
document.getElementById('tierModal').style.display = 'none';
loadTiers();
showToast('Tier saved successfully', 'success');
// Reload page to refresh server-side data
window.location.reload();
})
.catch(error => {
showToast('Error saving tier: ' + error.message, 'danger');
});
}
// Function to close modal
function closeTierModal() {
document.getElementById('tierModal').style.display = 'none';
}
function deleteTier(tierId) {
if (confirm('Are you sure you want to delete this tier? Users on this tier will be moved to the default free tier.')) {
fetch('{{ url_for(request, "/api/admin/tiers") }}/' + tierId, {
method: 'DELETE'
})
.then(response => response.json())
.then(result => {
showToast('Tier deleted successfully', 'success');
// Reload page to reflect changes
window.location.reload();
})
.catch(error => {
showToast('Error deleting tier', 'danger');
});
}
}
function loadCurrencySettings() {
fetch('{{ url_for(request, "/api/admin/settings/currency") }}')
.then(response => response.json())
.then(settings => {
document.getElementById('currencyCode').value = settings.currency_code;
document.getElementById('currencySymbol').value = settings.currency_symbol;
document.getElementById('currencyDecimals').value = settings.currency_decimals;
});
}
document.getElementById('currencySettingsForm').addEventListener('submit', function(e) {
e.preventDefault();
const currencyData = {
currency_code: document.getElementById('currencyCode').value,
currency_symbol: document.getElementById('currencySymbol').value,
currency_decimals: parseInt(document.getElementById('currencyDecimals').value)
};
fetch('{{ url_for(request, "/api/admin/settings/currency") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(currencyData)
})
.then(response => response.json())
.then(result => {
showToast('Currency settings saved successfully', 'success');
})
.catch(error => {
showToast('Error saving currency settings', 'danger');
});
});
function loadPaymentGateways() {
fetch('{{ url_for(request, "/api/admin/settings/payment-gateways") }}')
.then(response => response.json())
.then(gateways => {
// Load gateway configuration into form fields
if (gateways.paypal) {
document.getElementById('paypalEnabled').checked = gateways.paypal.enabled || false;
document.getElementById('paypalClientId').value = gateways.paypal.client_id || '';
document.getElementById('paypalClientSecret').value = gateways.paypal.client_secret || '';
document.getElementById('paypalWebhookSecret').value = gateways.paypal.webhook_secret || '';
document.getElementById('paypalSandbox').checked = gateways.paypal.sandbox !== false;
}
if (gateways.stripe) {
document.getElementById('stripeEnabled').checked = gateways.stripe.enabled || false;
document.getElementById('stripePublishableKey').value = gateways.stripe.publishable_key || '';
document.getElementById('stripeSecretKey').value = gateways.stripe.secret_key || '';
document.getElementById('stripeWebhookSecret').value = gateways.stripe.webhook_secret || '';
document.getElementById('stripeSandbox').checked = gateways.stripe.sandbox !== false;
}
if (gateways.bitcoin) {
document.getElementById('bitcoinEnabled').checked = gateways.bitcoin.enabled || false;
document.getElementById('bitcoinAddress').value = gateways.bitcoin.address || '';
document.getElementById('bitcoinConfirmations').value = gateways.bitcoin.confirmations || 3;
document.getElementById('bitcoinExpiration').value = gateways.bitcoin.expiration_minutes || 120;
}
if (gateways.ethereum) {
document.getElementById('ethEnabled').checked = gateways.ethereum.enabled || false;
document.getElementById('ethAddress').value = gateways.ethereum.address || '';
document.getElementById('ethConfirmations').value = gateways.ethereum.confirmations || 12;
document.getElementById('ethChainId').value = gateways.ethereum.chain_id || 1;
}
if (gateways.usdt) {
document.getElementById('usdtEnabled').checked = gateways.usdt.enabled || false;
document.getElementById('usdtAddress').value = gateways.usdt.address || '';
document.getElementById('usdtNetwork').value = gateways.usdt.network || 'erc20';
document.getElementById('usdtConfirmations').value = gateways.usdt.confirmations || 3;
}
if (gateways.usdc) {
document.getElementById('usdcEnabled').checked = gateways.usdc.enabled || false;
document.getElementById('usdcAddress').value = gateways.usdc.address || '';
document.getElementById('usdcNetwork').value = gateways.usdc.network || 'erc20';
document.getElementById('usdcConfirmations').value = gateways.usdc.confirmations || 3;
}
});
}
function savePaymentGateways() {
const gatewaysData = {
paypal: {
enabled: document.getElementById('paypalEnabled').checked,
client_id: document.getElementById('paypalClientId').value,
client_secret: document.getElementById('paypalClientSecret').value,
webhook_secret: document.getElementById('paypalWebhookSecret').value,
sandbox: document.getElementById('paypalSandbox').checked
},
stripe: {
enabled: document.getElementById('stripeEnabled').checked,
publishable_key: document.getElementById('stripePublishableKey').value,
secret_key: document.getElementById('stripeSecretKey').value,
webhook_secret: document.getElementById('stripeWebhookSecret').value,
sandbox: document.getElementById('stripeSandbox').checked
},
bitcoin: {
enabled: document.getElementById('bitcoinEnabled').checked,
address: document.getElementById('bitcoinAddress').value,
confirmations: parseInt(document.getElementById('bitcoinConfirmations').value),
expiration_minutes: parseInt(document.getElementById('bitcoinExpiration').value)
},
ethereum: {
enabled: document.getElementById('ethEnabled').checked,
address: document.getElementById('ethAddress').value,
confirmations: parseInt(document.getElementById('ethConfirmations').value),
chain_id: parseInt(document.getElementById('ethChainId').value)
},
usdt: {
enabled: document.getElementById('usdtEnabled').checked,
address: document.getElementById('usdtAddress').value,
network: document.getElementById('usdtNetwork').value,
confirmations: parseInt(document.getElementById('usdtConfirmations').value)
},
usdc: {
enabled: document.getElementById('usdcEnabled').checked,
address: document.getElementById('usdcAddress').value,
network: document.getElementById('usdcNetwork').value,
confirmations: parseInt(document.getElementById('usdcConfirmations').value)
}
};
fetch('{{ url_for(request, "/api/admin/settings/payment-gateways") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(gatewaysData)
})
.then(response => response.json())
.then(result => {
showToast('Payment gateway configuration saved successfully', 'success');
})
.catch(error => {
showToast('Error saving payment gateway configuration', 'danger');
});
}
function showToast(message, type) {
// Create and show toast notification
const toastContainer = document.createElement('div');
toastContainer.className = `position-fixed bottom-0 end-0 p-3`;
toastContainer.innerHTML = `
<div class="toast show" role="alert">
<div class="toast-header bg-${type} text-white">
<strong class="me-auto">Notification</strong>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="toast"></button>
</div>
<div class="toast-body">
${message}
</div>
</div>
`;
document.body.appendChild(toastContainer);
setTimeout(() => toastContainer.remove(), 3000);
}
</script>
{% endblock %}
\ No newline at end of file
{% extends "base.html" %}
{% block content %}
<h2 style="margin-bottom: 30px;">Billing & Payments</h2>
{% if success %}
<div class="alert alert-success">{{ success }}</div>
{% endif %}
{% if error %}
<div class="alert alert-error">{{ error }}</div>
{% endif %}
<!-- Payment Methods Section -->
<div style="background: #16213e; border: 2px solid #4a9eff; border-radius: 8px; padding: 20px; margin-bottom: 20px;">
<h3 style="margin: 0 0 20px 0; color: #4a9eff;">
<i class="fas fa-wallet me-2"></i>Payment Methods
</h3>
{% if payment_methods and payment_methods|length > 0 %}
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 15px;">
{% for method in payment_methods %}
<div class="payment-method-card {% if method.is_default %}default{% endif %}">
<div style="display: flex; align-items: center; margin-bottom: 10px;">
<div style="margin-right: 15px;">
{% if method.type == 'paypal' %}
<i class="fab fa-paypal fa-2x text-primary"></i>
{% elif method.type == 'stripe' %}
<i class="fab fa-cc-stripe fa-2x text-primary"></i>
{% elif method.type == 'bitcoin' %}
<i class="fab fa-bitcoin fa-2x text-warning"></i>
{% elif method.type == 'ethereum' or method.type == 'eth' %}
<i class="fab fa-ethereum fa-2x text-purple"></i>
{% elif method.type == 'usdt' %}
<i class="fas fa-coins fa-2x text-success"></i>
{% elif method.type == 'usdc' %}
<i class="fas fa-coins fa-2x text-info"></i>
{% else %}
<i class="fas fa-credit-card fa-2x text-secondary"></i>
{% endif %}
</div>
<div style="flex-grow: 1;">
<div style="font-weight: bold; margin-bottom: 5px;">{{ method.type|title }}</div>
<div style="color: #a0a0a0; font-size: 14px;">
{% if method.last4 %}
**** **** **** {{ method.last4 }}
{% else %}
{{ method.email if method.email else method.address }}
{% endif %}
</div>
{% if method.is_default %}
<span style="background: #28a745; color: white; padding: 2px 8px; border-radius: 10px; font-size: 12px; margin-top: 5px; display: inline-block;">Default</span>
{% endif %}
</div>
<div style="display: flex; flex-direction: column; gap: 5px;">
{% if not method.is_default %}
<button class="btn btn-secondary" style="padding: 5px 10px; font-size: 12px;">Set Default</button>
{% endif %}
<button class="btn btn-secondary" style="background: #dc3545; padding: 5px 10px; font-size: 12px;">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div style="text-align: center; padding: 40px; color: #a0a0a0;">
<i class="fas fa-credit-card fa-4x mb-3"></i>
<h4 style="margin-bottom: 15px;">No payment methods configured</h4>
<p style="margin-bottom: 20px;">Add a payment method to upgrade your plan and manage subscriptions</p>
<button class="btn" data-bs-toggle="modal" data-bs-target="#addPaymentMethodModal">Add Payment Method</button>
</div>
{% endif %}
<div style="display: flex; gap: 10px; margin-top: 20px;">
<button class="btn" data-bs-toggle="modal" data-bs-target="#addPaymentMethodModal">
<i class="fas fa-plus me-2"></i>Add Payment Method
</button>
</div>
</div>
<!-- Billing History Section -->
<div style="background: #16213e; border: 2px solid #17a2b8; border-radius: 8px; padding: 20px; margin-bottom: 20px;">
<h3 style="margin: 0 0 20px 0; color: #17a2b8;">
<i class="fas fa-history me-2"></i>Billing History
</h3>
{% if transactions and transactions|length > 0 %}
<div style="overflow-x: auto;">
<table style="width: 100%; border-collapse: collapse; margin-top: 20px;">
<thead>
<tr style="background: #0f3460; color: #e0e0e0;">
<th style="padding: 12px; text-align: center; border-bottom: 1px solid #0f3460; font-weight: 600;">Date</th>
<th style="padding: 12px; text-align: left; border-bottom: 1px solid #0f3460; font-weight: 600;">Description</th>
<th style="padding: 12px; text-align: center; border-bottom: 1px solid #0f3460; font-weight: 600;">Amount</th>
<th style="padding: 12px; text-align: center; border-bottom: 1px solid #0f3460; font-weight: 600;">Method</th>
<th style="padding: 12px; text-align: center; border-bottom: 1px solid #0f3460; font-weight: 600;">Status</th>
<th style="padding: 12px; text-align: center; border-bottom: 1px solid #0f3460; font-weight: 600;">Actions</th>
</tr>
</thead>
<tbody>
{% for tx in transactions %}
<tr style="border-bottom: 1px solid #0f3460; color: #e0e0e0;">
<td style="padding: 12px; text-align: center;">
<div style="font-weight: bold;">{{ tx.created_at.split(' ')[0] if tx.created_at else '' }}</div>
<small style="color: #a0a0a0;">{{ tx.created_at.split(' ')[1] if tx.created_at else '' }}</small>
</td>
<td style="padding: 12px;">
<div style="font-weight: bold;">
{% if tx.description %}
{{ tx.description }}
{% else %}
Plan Payment
{% endif %}
</div>
{% if tx.plan_name %}
<small style="color: #a0a0a0;">{{ tx.plan_name }}</small>
{% endif %}
</td>
<td style="padding: 12px; text-align: center;">
<span style="font-size: 18px; font-weight: bold; color: #4ade80;">
{% if tx.currency_symbol %}
{{ tx.currency_symbol }}
{% else %}
$
{% endif %}
{{ tx.amount }}
</span>
</td>
<td style="padding: 12px; text-align: center;">
{% if tx.payment_method == 'paypal' %}
<i class="fab fa-paypal text-primary"></i><br><small>PayPal</small>
{% elif tx.payment_method == 'stripe' %}
<i class="fab fa-cc-stripe text-primary"></i><br><small>Credit Card</small>
{% elif tx.payment_method == 'bitcoin' %}
<i class="fab fa-bitcoin text-warning"></i><br><small>Bitcoin</small>
{% elif tx.payment_method == 'eth' %}
<i class="fab fa-ethereum text-purple"></i><br><small>Ethereum</small>
{% elif tx.payment_method == 'usdt' %}
<i class="fas fa-coins text-success"></i><br><small>USDT</small>
{% elif tx.payment_method == 'usdc' %}
<i class="fas fa-coins text-info"></i><br><small>USDC</small>
{% else %}
<i class="fas fa-credit-card text-secondary"></i><br><small>{{ tx.payment_method|title }}</small>
{% endif %}
</td>
<td style="padding: 12px; text-align: center;">
{% if tx.status == 'completed' %}
<span style="background: #28a745; color: white; padding: 4px 8px; border-radius: 10px; font-size: 12px;">✓ Completed</span>
{% elif tx.status == 'pending' %}
<span style="background: #ffc107; color: black; padding: 4px 8px; border-radius: 10px; font-size: 12px;">⏳ Pending</span>
{% elif tx.status == 'failed' %}
<span style="background: #dc3545; color: white; padding: 4px 8px; border-radius: 10px; font-size: 12px;">✗ Failed</span>
{% elif tx.status == 'refunded' %}
<span style="background: #6c757d; color: white; padding: 4px 8px; border-radius: 10px; font-size: 12px;">↩ Refunded</span>
{% else %}
<span style="background: #6c757d; color: white; padding: 4px 8px; border-radius: 10px; font-size: 12px;">{{ tx.status|title }}</span>
{% endif %}
</td>
<td style="padding: 12px; text-align: center;">
{% if tx.invoice_id %}
<a href="#" class="btn btn-secondary" style="padding: 5px 10px; font-size: 12px;">
<i class="fas fa-file-pdf me-1"></i>Invoice
</a>
{% endif %}
<button class="btn btn-secondary" style="padding: 5px 10px; font-size: 12px; margin-left: 5px;" title="View Details">
<i class="fas fa-eye"></i>
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if transactions|length > 10 %}
<div style="margin-top: 20px; text-align: center;">
<button class="btn btn-secondary" style="margin: 0 5px;">Previous</button>
<button class="btn" style="margin: 0 5px;">1</button>
<button class="btn btn-secondary" style="margin: 0 5px;">2</button>
<button class="btn btn-secondary" style="margin: 0 5px;">Next</button>
</div>
{% endif %}
{% else %}
<div style="text-align: center; padding: 40px; color: #a0a0a0;">
<i class="fas fa-receipt fa-4x mb-3"></i>
<h4 style="margin-bottom: 15px;">No billing history yet</h4>
<p style="margin-bottom: 20px;">You don't have any payment transactions on your account.</p>
<p style="margin-bottom: 20px;">Upgrade your plan to get started!</p>
<a href="{{ url_for(request, '/dashboard/pricing') }}" class="btn">
<i class="fas fa-star me-2"></i>View Plans & Pricing
</a>
</div>
{% endif %}
</div>
{% endblock %}
<!-- Add Payment Method Modal -->
<div class="modal fade" id="addPaymentMethodModal" tabindex="-1" aria-labelledby="addPaymentMethodModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title" id="addPaymentMethodModalLabel">
<i class="fas fa-plus me-2"></i>Add Payment Method
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p class="text-muted mb-4">Choose a payment method to add to your account. You can use this for subscription payments and plan upgrades.</p>
<div class="row g-4">
{% if 'stripe' in enabled_gateways %}
<div class="col-md-6">
<div class="card h-100 border-primary">
<div class="card-body text-center">
<i class="fab fa-cc-stripe fa-4x mb-3 text-primary"></i>
<h5 class="card-title">Credit Card</h5>
<p class="card-text text-muted">Secure payment via Stripe</p>
<button class="btn btn-primary w-100">Connect with Stripe</button>
</div>
</div>
</div>
{% endif %}
{% if 'paypal' in enabled_gateways %}
<div class="col-md-6">
<div class="card h-100 border-primary">
<div class="card-body text-center">
<i class="fab fa-paypal fa-4x mb-3 text-primary"></i>
<h5 class="card-title">PayPal</h5>
<p class="card-text text-muted">Pay with your PayPal account</p>
<button class="btn btn-primary w-100">Connect with PayPal</button>
</div>
</div>
</div>
{% endif %}
{% if 'bitcoin' in enabled_gateways or 'eth' in enabled_gateways or 'usdt' in enabled_gateways or 'usdc' in enabled_gateways %}
<div class="col-12">
<div class="card border-warning">
<div class="card-header bg-warning text-dark">
<h6 class="mb-0">
<i class="fab fa-bitcoin me-2"></i>Cryptocurrency Payments
</h6>
</div>
<div class="card-body">
<p class="text-muted mb-3">Pay with cryptocurrency for instant processing</p>
<div class="row g-3">
{% if 'bitcoin' in enabled_gateways %}
<div class="col-md-3">
<button class="btn btn-outline-warning w-100">
<i class="fab fa-bitcoin fa-2x mb-2"></i><br>
<strong>Bitcoin</strong>
</button>
</div>
{% endif %}
{% if 'eth' in enabled_gateways %}
<div class="col-md-3">
<button class="btn btn-outline-purple w-100">
<i class="fab fa-ethereum fa-2x mb-2"></i><br>
<strong>Ethereum</strong>
</button>
</div>
{% endif %}
{% if 'usdt' in enabled_gateways %}
<div class="col-md-3">
<button class="btn btn-outline-success w-100">
<i class="fas fa-coins fa-2x mb-2"></i><br>
<strong>USDT</strong>
</button>
</div>
{% endif %}
{% if 'usdc' in enabled_gateways %}
<div class="col-md-3">
<button class="btn btn-outline-info w-100">
<i class="fas fa-coins fa-2x mb-2"></i><br>
<strong>USDC</strong>
</button>
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
<!-- Add Payment Method Modal -->
<div class="modal fade" id="addPaymentMethodModal" tabindex="-1" aria-labelledby="addPaymentMethodModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title" id="addPaymentMethodModalLabel">
<i class="fas fa-plus me-2"></i>Add Payment Method
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p class="text-muted mb-4">Choose a payment method to add to your account. You can use this for subscription payments and plan upgrades.</p>
<div class="row g-4">
{% if 'stripe' in enabled_gateways %}
<div class="col-md-6">
<div class="card h-100 border-primary">
<div class="card-body text-center">
<i class="fab fa-cc-stripe fa-4x mb-3 text-primary"></i>
<h5 class="card-title">Credit Card</h5>
<p class="card-text text-muted">Secure payment via Stripe</p>
<button class="btn btn-primary w-100">Connect with Stripe</button>
</div>
</div>
</div>
{% endif %}
{% if 'paypal' in enabled_gateways %}
<div class="col-md-6">
<div class="card h-100 border-primary">
<div class="card-body text-center">
<i class="fab fa-paypal fa-4x mb-3 text-primary"></i>
<h5 class="card-title">PayPal</h5>
<p class="card-text text-muted">Pay with your PayPal account</p>
<button class="btn btn-primary w-100">Connect with PayPal</button>
</div>
</div>
</div>
{% endif %}
{% if 'bitcoin' in enabled_gateways or 'eth' in enabled_gateways or 'usdt' in enabled_gateways or 'usdc' in enabled_gateways %}
<div class="col-12">
<div class="card border-warning">
<div class="card-header bg-warning text-dark">
<h6 class="mb-0">
<i class="fab fa-bitcoin me-2"></i>Cryptocurrency Payments
</h6>
</div>
<div class="card-body">
<p class="text-muted mb-3">Pay with cryptocurrency for instant processing</p>
<div class="row g-3">
{% if 'bitcoin' in enabled_gateways %}
<div class="col-md-3">
<button class="btn btn-outline-warning w-100">
<i class="fab fa-bitcoin fa-2x mb-2"></i><br>
<strong>Bitcoin</strong>
</button>
</div>
{% endif %}
{% if 'eth' in enabled_gateways %}
<div class="col-md-3">
<button class="btn btn-outline-purple w-100">
<i class="fab fa-ethereum fa-2x mb-2"></i><br>
<strong>Ethereum</strong>
</button>
</div>
{% endif %}
{% if 'usdt' in enabled_gateways %}
<div class="col-md-3">
<button class="btn btn-outline-success w-100">
<i class="fas fa-coins fa-2x mb-2"></i><br>
<strong>USDT</strong>
</button>
</div>
{% endif %}
{% if 'usdc' in enabled_gateways %}
<div class="col-md-3">
<button class="btn btn-outline-info w-100">
<i class="fas fa-coins fa-2x mb-2"></i><br>
<strong>USDC</strong>
</button>
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% block extra_css %}
<style>
/* Payment Method Cards */
.payment-method-card {
border: 1px solid #0f3460;
border-radius: 5px;
padding: 15px;
background: #1a1a2e;
margin-bottom: 10px;
transition: all 0.2s ease;
}
.payment-method-card:hover {
border-color: #4a9eff;
box-shadow: 0 2px 8px rgba(74, 158, 255, 0.2);
}
.payment-method-card.default {
border-color: #28a745;
box-shadow: 0 0 10px rgba(40, 167, 69, 0.3);
}
.text-purple {
color: #6f42c1;
}
/* Form groups */
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: 500;
color: #e0e0e0;
}
.form-group input[type="text"],
.form-group input[type="password"],
.form-group input[type="number"],
.form-group select {
width: 100%;
padding: 8px;
border: 1px solid #0f3460;
border-radius: 3px;
font-size: 14px;
background: #1a1a2e;
color: #e0e0e0;
}
.form-group input[type="checkbox"] {
margin-right: 5px;
}
</style>
{% endblock %}
{% extends "base.html" %}
{% block title %}Pricing Plans{% endblock %}
{% block content %}
<h2 style="margin-bottom: 30px;">Pricing Plans</h2>
{% if success %}
<div class="alert alert-success">{{ success }}</div>
{% endif %}
{% if error %}
<div class="alert alert-error">{{ error }}</div>
{% endif %}
<div style="text-align: center; margin-bottom: 40px;">
<h3 style="margin-bottom: 15px; color: #e0e0e0;">Choose Your Plan</h3>
<p style="color: #a0a0a0; font-size: 16px; max-width: 600px; margin: 0 auto;">Select the perfect tier for your AI service needs. All plans include access to all providers, rotations, and autoselection features.</p>
</div>
<!-- Paid Tiers -->
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; margin-bottom: 40px;">
{% for tier in tiers %}
{% if tier.is_active and not tier.is_default %}
<div class="pricing-card {% if tier.is_recommended %}recommended{% endif %}">
{% if tier.is_recommended %}
<div style="background: #4a9eff; color: white; padding: 8px 15px; border-radius: 20px; text-align: center; margin-bottom: 20px; font-weight: bold; text-transform: uppercase; font-size: 12px;">
Recommended
</div>
{% endif %}
<div style="text-align: center; margin-bottom: 25px;">
<h3 style="margin: 0 0 15px 0; color: #e0e0e0;">{{ tier.name }}</h3>
<div style="font-size: 32px; font-weight: bold; color: #4ade80; margin-bottom: 5px;">
{{ currency_symbol }}{{ "%.2f"|format(tier.price_monthly) }}
</div>
<div style="color: #a0a0a0;">per month</div>
{% if tier.price_yearly > 0 %}
<div style="color: #4ade80; font-size: 12px; margin-top: 5px;">
Save {{ ((1 - tier.price_yearly / (tier.price_monthly * 12)) * 100)|round|int }}% with annual billing
</div>
{% endif %}
</div>
<ul style="list-style: none; padding: 0; margin: 0 0 25px 0;">
<li style="margin-bottom: 10px; color: #e0e0e0;">
<i class="fas fa-check" style="color: #4ade80; margin-right: 10px;"></i>
{% if tier.max_requests_day == -1 %}
Unlimited requests per day
{% elif tier.max_requests_day == 0 %}
No requests permitted
{% else %}
{{ tier.max_requests_day }} requests per day
{% endif %}
</li>
<li style="margin-bottom: 10px; color: #e0e0e0;">
<i class="fas fa-check" style="color: #4ade80; margin-right: 10px;"></i>
{% if tier.max_requests_month == -1 %}
Unlimited requests per month
{% elif tier.max_requests_month == 0 %}
No requests permitted
{% else %}
{{ tier.max_requests_month }} requests per month
{% endif %}
</li>
<li style="margin-bottom: 10px; color: #e0e0e0;">
<i class="fas fa-check" style="color: #4ade80; margin-right: 10px;"></i>
{% if tier.max_providers == -1 %}
Unlimited providers
{% elif tier.max_providers == 0 %}
No providers permitted
{% else %}
Up to {{ tier.max_providers }} providers
{% endif %}
</li>
<li style="margin-bottom: 10px; color: #e0e0e0;">
<i class="fas fa-check" style="color: #4ade80; margin-right: 10px;"></i>
{% if tier.max_rotations == -1 %}
Unlimited rotations
{% elif tier.max_rotations == 0 %}
No rotations permitted
{% else %}
Up to {{ tier.max_rotations }} rotations
{% endif %}
</li>
<li style="margin-bottom: 10px; color: #e0e0e0;">
<i class="fas fa-check" style="color: #4ade80; margin-right: 10px;"></i>
{% if tier.max_autoselections == -1 %}
Unlimited autoselections
{% elif tier.max_autoselections == 0 %}
No autoselections permitted
{% else %}
Up to {{ tier.max_autoselections }} autoselections
{% endif %}
</li>
</ul>
<div style="text-align: center;">
{% if current_tier and current_tier.id == tier.id %}
<button class="btn btn-secondary" disabled style="width: 100%; padding: 12px;">
<i class="fas fa-check me-2"></i>Current Plan
</button>
{% else %}
<button class="btn upgrade-btn" onclick="subscribeToTier({{ tier.id }})" style="width: 100%; padding: 12px;">
<i class="fas fa-arrow-up me-2"></i>Upgrade Now
</button>
{% endif %}
</div>
</div>
{% endif %}
{% endfor %}
</div>
<!-- Free Tier -->
<div style="max-width: 500px; margin: 0 auto;">
<div style="background: #16213e; border: 2px solid #0f3460; border-radius: 8px; padding: 30px; text-align: center;">
<h3 style="margin: 0 0 15px 0; color: #e0e0e0;">Free Tier</h3>
<div style="font-size: 36px; font-weight: bold; color: #4ade80; margin-bottom: 10px;">Free Forever</div>
<p style="color: #a0a0a0; margin-bottom: 20px;">Perfect for testing and personal use</p>
<ul style="list-style: none; padding: 0; margin: 0 0 25px 0; text-align: left; max-width: 300px; margin-left: auto; margin-right: auto;">
{% set free_tier = tiers|selectattr('is_default', 'equalto', True)|first %}
{% if free_tier %}
<li style="margin-bottom: 8px; color: #e0e0e0;">
<i class="fas fa-check" style="color: #4ade80; margin-right: 8px;"></i>
{% if free_tier.max_requests_day == -1 %}
Unlimited requests per day
{% else %}
{{ free_tier.max_requests_day }} requests per day
{% endif %}
</li>
<li style="margin-bottom: 8px; color: #e0e0e0;">
<i class="fas fa-check" style="color: #4ade80; margin-right: 8px;"></i>
{% if free_tier.max_requests_month == -1 %}
Unlimited requests per month
{% else %}
{{ free_tier.max_requests_month }} requests per month
{% endif %}
</li>
<li style="margin-bottom: 8px; color: #e0e0e0;">
<i class="fas fa-check" style="color: #4ade80; margin-right: 8px;"></i>
{% if free_tier.max_providers == -1 %}
Unlimited providers
{% else %}
Up to {{ free_tier.max_providers }} providers
{% endif %}
</li>
{% endif %}
</ul>
{% if not current_tier or current_tier.is_default %}
<button class="btn btn-secondary" disabled style="width: 100%; padding: 12px;">
<i class="fas fa-check me-2"></i>You are on the Free Plan
</button>
{% else %}
<button class="btn btn-secondary" onclick="downgradeToFree()" style="width: 100%; padding: 12px; background: #dc3545;">
<i class="fas fa-arrow-down me-2"></i>Downgrade to Free
</button>
{% endif %}
</div>
</div>
<!-- Payment Methods -->
{% if enabled_gateways|length > 0 %}
<div style="background: #16213e; border: 2px solid #17a2b8; border-radius: 8px; padding: 25px; margin-top: 40px; text-align: center;">
<h4 style="margin: 0 0 20px 0; color: #17a2b8;">Secure Payment Methods</h4>
<div style="display: flex; justify-content: center; align-items: center; gap: 30px; flex-wrap: wrap;">
{% for gateway in enabled_gateways %}
<div style="text-align: center;">
{% if gateway == 'paypal' %}
<i class="fab fa-paypal fa-3x" style="color: #4a9eff; margin-bottom: 8px;"></i>
<div style="color: #a0a0a0; font-size: 14px;">PayPal</div>
{% elif gateway == 'stripe' %}
<i class="fab fa-stripe fa-3x" style="color: #4ade80; margin-bottom: 8px;"></i>
<div style="color: #a0a0a0; font-size: 14px;">Stripe</div>
{% elif gateway == 'bitcoin' %}
<i class="fab fa-bitcoin fa-3x" style="color: #f39c12; margin-bottom: 8px;"></i>
<div style="color: #a0a0a0; font-size: 14px;">Bitcoin</div>
{% elif gateway == 'ethereum' %}
<i class="fab fa-ethereum fa-3x" style="color: #6f42c1; margin-bottom: 8px;"></i>
<div style="color: #a0a0a0; font-size: 14px;">Ethereum</div>
{% elif gateway == 'usdt' %}
<i class="fas fa-dollar-sign fa-3x" style="color: #17a2b8; margin-bottom: 8px;"></i>
<div style="color: #a0a0a0; font-size: 14px;">USDT</div>
{% elif gateway == 'usdc' %}
<i class="fas fa-coins fa-3x" style="color: #4a9eff; margin-bottom: 8px;"></i>
<div style="color: #a0a0a0; font-size: 14px;">USDC</div>
{% endif %}
</div>
{% endfor %}
</div>
<p style="color: #a0a0a0; font-size: 14px; margin-top: 15px;">All transactions are secure and encrypted</p>
</div>
{% endif %}
{% endblock %}
{% block extra_css %}
<style>
.pricing-card {
background: #16213e;
border: 2px solid #0f3460;
border-radius: 8px;
padding: 25px;
}
.pricing-card.recommended {
border-color: #4a9eff;
box-shadow: 0 0 20px rgba(74, 158, 255, 0.3);
}
.upgrade-btn {
background: #4a9eff;
}
</style>
{% endblock %}
{% block extra_js %}
<script>
function subscribeToTier(tierId) {
if (confirm('Are you sure you want to upgrade to this tier?')) {
fetch(url_for(`/api/subscribe/${tierId}`), {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(result => {
if (result.payment_url) {
window.location.href = result.payment_url;
} else if (result.success) {
showToast('Subscription updated successfully', 'success');
setTimeout(() => window.location.reload(), 1500);
} else {
showToast(result.error || 'Error processing subscription', 'danger');
}
})
.catch(error => {
showToast('Error processing subscription', 'danger');
});
}
}
function downgradeToFree() {
if (confirm('Are you sure you want to downgrade to the free tier? Your current subscription will remain active until the end of the billing period.')) {
fetch(url_for('/api/subscribe/free'), {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(result => {
showToast('Your account will be downgraded at the end of the billing period', 'success');
setTimeout(() => window.location.reload(), 1500);
})
.catch(error => {
showToast('Error processing downgrade', 'danger');
});
}
}
function showToast(message, type) {
const toastContainer = document.createElement('div');
toastContainer.className = `position-fixed bottom-0 end-0 p-3`;
toastContainer.innerHTML = `
<div class="toast show" role="alert">
<div class="toast-header bg-${type} text-white">
<strong class="me-auto">Notification</strong>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="toast"></button>
</div>
<div class="toast-body">
${message}
</div>
</div>
`;
document.body.appendChild(toastContainer);
setTimeout(() => toastContainer.remove(), 3000);
}
</script>
{% endblock %}
\ No newline at end of file
......@@ -287,6 +287,100 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</div>
</div>
<h3 style="margin: 30px 0 20px;">Response Cache Configuration</h3>
<div class="form-group">
<label>
<input type="checkbox" name="response_cache_enabled" {% if config.response_cache and config.response_cache.enabled %}checked{% endif %}>
Enable Response Cache
</label>
<small style="color: #666; display: block; margin-top: 5px;">Cache API responses to improve performance and reduce costs</small>
</div>
<div class="form-group">
<label for="response_cache_backend">Response Cache Backend</label>
<select id="response_cache_backend" name="response_cache_backend" onchange="toggleResponseCacheFields()">
<option value="memory" {% if config.response_cache and config.response_cache.backend == 'memory' %}selected{% endif %}>Memory</option>
<option value="sqlite" {% if config.response_cache and config.response_cache.backend == 'sqlite' %}selected{% endif %}>SQLite</option>
<option value="redis" {% if config.response_cache and config.response_cache.backend == 'redis' %}selected{% endif %}>Redis</option>
<option value="mysql" {% if config.response_cache and config.response_cache.backend == 'mysql' %}selected{% endif %}>MySQL</option>
</select>
<small style="color: #666; display: block; margin-top: 5px;">Choose the backend for response caching</small>
</div>
<div class="form-group">
<label for="response_cache_ttl">Cache TTL (seconds)</label>
<input type="number" id="response_cache_ttl" name="response_cache_ttl" value="{{ config.response_cache.ttl if config.response_cache else 600 }}" min="1">
<small style="color: #666; display: block; margin-top: 5px;">Time to live for cached responses (default: 600 seconds)</small>
</div>
<div class="form-group">
<label for="response_cache_max_memory">Max Memory Cache Size</label>
<input type="number" id="response_cache_max_memory" name="response_cache_max_memory" value="{{ config.response_cache.max_memory_cache if config.response_cache else 1000 }}" min="1">
<small style="color: #666; display: block; margin-top: 5px;">Maximum number of items in memory cache (default: 1000)</small>
</div>
<div id="response-cache-redis-fields" style="display: {% if config.response_cache and config.response_cache.backend == 'redis' %}block{% else %}none{% endif %};">
<div class="form-group">
<label for="response_cache_redis_host">Redis Host</label>
<input type="text" id="response_cache_redis_host" name="response_cache_redis_host" value="{{ config.response_cache.redis_host if config.response_cache else 'localhost' }}">
</div>
<div class="form-group">
<label for="response_cache_redis_port">Redis Port</label>
<input type="number" id="response_cache_redis_port" name="response_cache_redis_port" value="{{ config.response_cache.redis_port if config.response_cache else 6379 }}">
</div>
<div class="form-group">
<label for="response_cache_redis_db">Redis Database</label>
<input type="number" id="response_cache_redis_db" name="response_cache_redis_db" value="{{ config.response_cache.redis_db if config.response_cache else 0 }}">
</div>
<div class="form-group">
<label for="response_cache_redis_password">Redis Password</label>
<input type="password" id="response_cache_redis_password" name="response_cache_redis_password" placeholder="Leave blank if no password">
</div>
<div class="form-group">
<label for="response_cache_redis_key_prefix">Redis Key Prefix</label>
<input type="text" id="response_cache_redis_key_prefix" name="response_cache_redis_key_prefix" value="{{ config.response_cache.redis_key_prefix if config.response_cache else 'aisbf:response:' }}">
</div>
</div>
<div id="response-cache-sqlite-fields" style="display: {% if config.response_cache and config.response_cache.backend == 'sqlite' %}block{% else %}none{% endif %};">
<div class="form-group">
<label for="response_cache_sqlite_path">SQLite Path</label>
<input type="text" id="response_cache_sqlite_path" name="response_cache_sqlite_path" value="{{ config.response_cache.sqlite_path if config.response_cache else '~/.aisbf/response_cache.db' }}">
</div>
</div>
<div id="response-cache-mysql-fields" style="display: {% if config.response_cache and config.response_cache.backend == 'mysql' %}block{% else %}none{% endif %};">
<div class="form-group">
<label for="response_cache_mysql_host">MySQL Host</label>
<input type="text" id="response_cache_mysql_host" name="response_cache_mysql_host" value="{{ config.response_cache.mysql_host if config.response_cache else 'localhost' }}">
</div>
<div class="form-group">
<label for="response_cache_mysql_port">MySQL Port</label>
<input type="number" id="response_cache_mysql_port" name="response_cache_mysql_port" value="{{ config.response_cache.mysql_port if config.response_cache else 3306 }}">
</div>
<div class="form-group">
<label for="response_cache_mysql_user">MySQL Username</label>
<input type="text" id="response_cache_mysql_user" name="response_cache_mysql_user" value="{{ config.response_cache.mysql_user if config.response_cache else 'aisbf' }}">
</div>
<div class="form-group">
<label for="response_cache_mysql_password">MySQL Password</label>
<input type="password" id="response_cache_mysql_password" name="response_cache_mysql_password" placeholder="Leave blank to keep current">
</div>
<div class="form-group">
<label for="response_cache_mysql_database">MySQL Database</label>
<input type="text" id="response_cache_mysql_database" name="response_cache_mysql_database" value="{{ config.response_cache.mysql_database if config.response_cache else 'aisbf_response_cache' }}">
</div>
</div>
<h3 style="margin: 30px 0 20px;">Response Cache Statistics</h3>
<div id="cache-stats" style="margin-bottom: 20px; padding: 15px; background: #0f3460; border-radius: 6px; border-left: 4px solid #16213e;">
......@@ -547,6 +641,138 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<small style="color: #666; display: block; margin-top: 5px;">Display name for sent emails</small>
</div>
<h3 style="margin: 30px 0 20px;">Request Batching</h3>
<div class="form-group">
<label>
<input type="checkbox" name="batching_enabled" {% if config.batching and config.batching.enabled %}checked{% endif %}>
Enable Request Batching
</label>
<small style="color: #666; display: block; margin-top: 5px;">Batch multiple requests together for improved efficiency</small>
</div>
<div class="form-group">
<label for="batching_window_ms">Batching Window (milliseconds)</label>
<input type="number" id="batching_window_ms" name="batching_window_ms" value="{{ config.batching.window_ms if config.batching else 100 }}" min="1">
<small style="color: #666; display: block; margin-top: 5px;">Time window to collect requests for batching (default: 100ms)</small>
</div>
<div class="form-group">
<label for="batching_max_batch_size">Max Batch Size</label>
<input type="number" id="batching_max_batch_size" name="batching_max_batch_size" value="{{ config.batching.max_batch_size if config.batching else 8 }}" min="1">
<small style="color: #666; display: block; margin-top: 5px;">Maximum number of requests per batch (default: 8)</small>
</div>
<h4 style="margin: 20px 0 15px;">Provider-Specific Batching Settings</h4>
<div class="form-group">
<label>
<input type="checkbox" name="batching_openai_enabled" {% if config.batching and config.batching.provider_settings and config.batching.provider_settings.openai and config.batching.provider_settings.openai.enabled %}checked{% endif %}>
Enable OpenAI Batching
</label>
</div>
<div class="form-group">
<label for="batching_openai_max_batch_size">OpenAI Max Batch Size</label>
<input type="number" id="batching_openai_max_batch_size" name="batching_openai_max_batch_size" value="{{ config.batching.provider_settings.openai.max_batch_size if config.batching and config.batching.provider_settings and config.batching.provider_settings.openai else 10 }}" min="1">
</div>
<div class="form-group">
<label>
<input type="checkbox" name="batching_anthropic_enabled" {% if config.batching and config.batching.provider_settings and config.batching.provider_settings.anthropic and config.batching.provider_settings.anthropic.enabled %}checked{% endif %}>
Enable Anthropic Batching
</label>
</div>
<div class="form-group">
<label for="batching_anthropic_max_batch_size">Anthropic Max Batch Size</label>
<input type="number" id="batching_anthropic_max_batch_size" name="batching_anthropic_max_batch_size" value="{{ config.batching.provider_settings.anthropic.max_batch_size if config.batching and config.batching.provider_settings and config.batching.provider_settings.anthropic else 5 }}" min="1">
</div>
<h3 style="margin: 30px 0 20px;">Adaptive Rate Limiting</h3>
<div class="form-group">
<label>
<input type="checkbox" name="adaptive_rate_limiting_enabled" {% if config.adaptive_rate_limiting and config.adaptive_rate_limiting.enabled %}checked{% endif %}>
Enable Adaptive Rate Limiting
</label>
<small style="color: #666; display: block; margin-top: 5px;">Automatically adjust rate limits based on provider responses</small>
</div>
<div class="form-group">
<label for="adaptive_initial_rate_limit">Initial Rate Limit (requests/second)</label>
<input type="number" id="adaptive_initial_rate_limit" name="adaptive_initial_rate_limit" value="{{ config.adaptive_rate_limiting.initial_rate_limit if config.adaptive_rate_limiting else 0 }}" min="0" step="0.1">
<small style="color: #666; display: block; margin-top: 5px;">Starting rate limit (0 = unlimited)</small>
</div>
<div class="form-group">
<label for="adaptive_learning_rate">Learning Rate</label>
<input type="number" id="adaptive_learning_rate" name="adaptive_learning_rate" value="{{ config.adaptive_rate_limiting.learning_rate if config.adaptive_rate_limiting else 0.1 }}" min="0" max="1" step="0.01">
<small style="color: #666; display: block; margin-top: 5px;">How quickly to adapt to rate limit changes (0-1, default: 0.1)</small>
</div>
<div class="form-group">
<label for="adaptive_headroom_percent">Headroom Percent</label>
<input type="number" id="adaptive_headroom_percent" name="adaptive_headroom_percent" value="{{ config.adaptive_rate_limiting.headroom_percent if config.adaptive_rate_limiting else 10 }}" min="0" max="100">
<small style="color: #666; display: block; margin-top: 5px;">Safety margin below detected limit (default: 10%)</small>
</div>
<div class="form-group">
<label for="adaptive_recovery_rate">Recovery Rate</label>
<input type="number" id="adaptive_recovery_rate" name="adaptive_recovery_rate" value="{{ config.adaptive_rate_limiting.recovery_rate if config.adaptive_rate_limiting else 0.05 }}" min="0" max="1" step="0.01">
<small style="color: #666; display: block; margin-top: 5px;">Rate at which limits recover after errors (default: 0.05)</small>
</div>
<div class="form-group">
<label for="adaptive_max_rate_limit">Max Rate Limit (requests/second)</label>
<input type="number" id="adaptive_max_rate_limit" name="adaptive_max_rate_limit" value="{{ config.adaptive_rate_limiting.max_rate_limit if config.adaptive_rate_limiting else 60 }}" min="0" step="0.1">
<small style="color: #666; display: block; margin-top: 5px;">Maximum allowed rate limit (default: 60)</small>
</div>
<div class="form-group">
<label for="adaptive_min_rate_limit">Min Rate Limit (requests/second)</label>
<input type="number" id="adaptive_min_rate_limit" name="adaptive_min_rate_limit" value="{{ config.adaptive_rate_limiting.min_rate_limit if config.adaptive_rate_limiting else 0.1 }}" min="0" step="0.1">
<small style="color: #666; display: block; margin-top: 5px;">Minimum allowed rate limit (default: 0.1)</small>
</div>
<div class="form-group">
<label for="adaptive_backoff_base">Backoff Base</label>
<input type="number" id="adaptive_backoff_base" name="adaptive_backoff_base" value="{{ config.adaptive_rate_limiting.backoff_base if config.adaptive_rate_limiting else 2 }}" min="1" step="0.1">
<small style="color: #666; display: block; margin-top: 5px;">Exponential backoff multiplier (default: 2)</small>
</div>
<div class="form-group">
<label for="adaptive_jitter_factor">Jitter Factor</label>
<input type="number" id="adaptive_jitter_factor" name="adaptive_jitter_factor" value="{{ config.adaptive_rate_limiting.jitter_factor if config.adaptive_rate_limiting else 0.25 }}" min="0" max="1" step="0.01">
<small style="color: #666; display: block; margin-top: 5px;">Random jitter to prevent thundering herd (0-1, default: 0.25)</small>
</div>
<div class="form-group">
<label for="adaptive_history_window">History Window (seconds)</label>
<input type="number" id="adaptive_history_window" name="adaptive_history_window" value="{{ config.adaptive_rate_limiting.history_window if config.adaptive_rate_limiting else 3600 }}" min="1">
<small style="color: #666; display: block; margin-top: 5px;">Time window for tracking rate limit history (default: 3600)</small>
</div>
<div class="form-group">
<label for="adaptive_consecutive_successes">Consecutive Successes for Recovery</label>
<input type="number" id="adaptive_consecutive_successes" name="adaptive_consecutive_successes" value="{{ config.adaptive_rate_limiting.consecutive_successes_for_recovery if config.adaptive_rate_limiting else 10 }}" min="1">
<small style="color: #666; display: block; margin-top: 5px;">Number of successful requests before increasing rate limit (default: 10)</small>
</div>
<h3 style="margin: 30px 0 20px;">Admin Password</h3>
<div class="form-group">
<label for="new_admin_password">New Admin Password</label>
<input type="password" id="new_admin_password" name="new_admin_password" placeholder="Leave blank to keep current password">
<small style="color: #666; display: block; margin-top: 5px;">Enter a new password to change the admin dashboard password</small>
</div>
<div class="form-group">
<label for="confirm_admin_password">Confirm New Admin Password</label>
<input type="password" id="confirm_admin_password" name="confirm_admin_password" placeholder="Confirm new password">
<small style="color: #666; display: block; margin-top: 5px;">Re-enter the new password to confirm</small>
</div>
<div style="display: flex; gap: 10px; margin-top: 30px;">
<button type="submit" class="btn">Save Settings</button>
<a href="{{ url_for(request, '/dashboard') }}" class="btn btn-secondary">Cancel</a>
......@@ -657,6 +883,27 @@ function toggleCacheFields() {
}
}
function toggleResponseCacheFields() {
const backend = document.getElementById('response_cache_backend').value;
const redisFields = document.getElementById('response-cache-redis-fields');
const sqliteFields = document.getElementById('response-cache-sqlite-fields');
const mysqlFields = document.getElementById('response-cache-mysql-fields');
// Hide all fields first
redisFields.style.display = 'none';
sqliteFields.style.display = 'none';
mysqlFields.style.display = 'none';
// Show only the relevant fields
if (backend === 'redis') {
redisFields.style.display = 'block';
} else if (backend === 'sqlite') {
sqliteFields.style.display = 'block';
} else if (backend === 'mysql') {
mysqlFields.style.display = 'block';
}
}
function createPersistentService() {
const dirInput = document.getElementById('tor_hidden_service_dir');
const defaultDir = '~/.aisbf/tor_hidden_service';
......
{% extends "base.html" %}
{% block content %}
<h2 style="margin-bottom: 30px;">Subscription Management</h2>
{% if success %}
<div class="alert alert-success">{{ success }}</div>
{% endif %}
{% if error %}
<div class="alert alert-error">{{ error }}</div>
{% endif %}
<!-- Current Plan Status -->
<div style="background: #16213e; border: 2px solid #4a9eff; border-radius: 8px; padding: 20px; margin-bottom: 20px;">
<h3 style="margin: 0 0 20px 0; color: #4a9eff;">
<i class="fas fa-star me-2"></i>Current Plan
</h3>
{% if current_tier %}
<div style="display: flex; align-items: center; margin-bottom: 20px;">
<div style="flex: 1;">
<div style="display: flex; align-items: center; margin-bottom: 10px;">
<h3 style="margin: 0 15px 0 0;">{{ current_tier.name }}</h3>
{% if current_tier.is_default %}
<span style="background: #28a745; color: white; padding: 4px 12px; border-radius: 15px; font-size: 12px;">Free Tier</span>
{% endif %}
</div>
<p style="color: #a0a0a0; margin: 0;">{{ current_tier.description if current_tier.description else 'No description available' }}</p>
</div>
<div style="text-align: right;">
{% if not current_tier.is_default %}
<div style="margin-bottom: 10px;">
<span style="font-size: 24px; font-weight: bold; color: #4ade80;">{{ currency_symbol }}{{ current_tier.price_monthly }}</span>
<span style="color: #a0a0a0; font-size: 14px;">/month</span>
<br>
<small style="color: #a0a0a0;">or {{ currency_symbol }}{{ current_tier.price_yearly }}/year</small>
</div>
{% endif %}
<a href="{{ url_for(request, '/dashboard/pricing') }}" class="btn">
<i class="fas fa-arrow-up me-2"></i>Change Plan
</a>
</div>
</div>
<!-- Plan Limits -->
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-bottom: 20px;">
<div style="text-align: center; padding: 15px; background: #0f2840; border-radius: 5px;">
<div style="font-size: 24px; font-weight: bold; color: #4a9eff; margin-bottom: 5px;">
{% if current_tier.max_requests_day == -1 %}
{% else %}
{{ current_tier.max_requests_day }}
{% endif %}
</div>
<div style="color: #a0a0a0; font-size: 12px;">Requests per day</div>
</div>
<div style="text-align: center; padding: 15px; background: #0f2840; border-radius: 5px;">
<div style="font-size: 24px; font-weight: bold; color: #4ade80; margin-bottom: 5px;">
{% if current_tier.max_requests_month == -1 %}
{% else %}
{{ current_tier.max_requests_month }}
{% endif %}
</div>
<div style="color: #a0a0a0; font-size: 12px;">Requests per month</div>
</div>
<div style="text-align: center; padding: 15px; background: #0f2840; border-radius: 5px;">
<div style="font-size: 24px; font-weight: bold; color: #f39c12; margin-bottom: 5px;">
{% if current_tier.max_providers == -1 %}
{% else %}
{{ current_tier.max_providers }}
{% endif %}
</div>
<div style="color: #a0a0a0; font-size: 12px;">Providers</div>
</div>
<div style="text-align: center; padding: 15px; background: #0f2840; border-radius: 5px;">
<div style="font-size: 24px; font-weight: bold; color: #17a2b8; margin-bottom: 5px;">
{% if current_tier.max_rotations == -1 %}
{% else %}
{{ current_tier.max_rotations }}
{% endif %}
</div>
<div style="color: #a0a0a0; font-size: 12px;">Rotations</div>
</div>
</div>
{% if subscription %}
<hr style="border-color: #0f3460; margin: 20px 0;">
<div style="display: flex; align-items: center;">
<div style="flex: 1;">
<h4 style="margin: 0 0 10px 0; color: #8ec8ff;">Subscription Status</h4>
<div style="display: flex; align-items: center; gap: 15px;">
<span class="subscription-status-{{ subscription.status }}">
{{ subscription.status|title }}
</span>
{% if subscription.expires_at %}
<div style="color: #a0a0a0;">
<i class="fas fa-calendar-alt me-1"></i>
<strong>Renews:</strong> {{ subscription.expires_at }}
</div>
{% endif %}
</div>
</div>
<div style="text-align: right;">
{% if subscription.status == 'active' %}
<button class="btn btn-secondary" style="background: #dc3545;">
<i class="fas fa-times-circle me-2"></i>Cancel Subscription
</button>
{% endif %}
</div>
</div>
{% endif %}
{% endif %}
</div>
<!-- Quick Actions -->
<div style="background: #16213e; border: 2px solid #8ec8ff; border-radius: 8px; padding: 20px; margin-bottom: 20px;">
<h3 style="margin: 0 0 20px 0; color: #8ec8ff;">
<i class="fas fa-bolt me-2"></i>Quick Actions
</h3>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 15px;">
<a href="{{ url_for(request, '/dashboard/billing') }}" style="text-decoration: none; color: inherit;">
<div style="border: 1px solid #0f3460; border-radius: 5px; padding: 20px; background: #1a1a2e; transition: all 0.2s ease; cursor: pointer;" onmouseover="this.style.borderColor='#4a9eff'; this.style.boxShadow='0 2px 8px rgba(74,158,255,0.2)';" onmouseout="this.style.borderColor='#0f3460'; this.style.boxShadow='none';">
<div style="text-align: center;">
<i class="fas fa-history fa-3x mb-3" style="color: #4a9eff;"></i>
<h4 style="margin: 0 0 10px 0;">Billing & Payments</h4>
<p style="margin: 0; color: #a0a0a0; font-size: 14px;">Manage payment methods and view history</p>
</div>
</div>
</a>
<a href="{{ url_for(request, '/dashboard/pricing') }}" style="text-decoration: none; color: inherit;">
<div style="border: 1px solid #0f3460; border-radius: 5px; padding: 20px; background: #1a1a2e; transition: all 0.2s ease; cursor: pointer;" onmouseover="this.style.borderColor='#f39c12'; this.style.boxShadow='0 2px 8px rgba(243,156,18,0.2)';" onmouseout="this.style.borderColor='#0f3460'; this.style.boxShadow='none';">
<div style="text-align: center;">
<i class="fas fa-star fa-3x mb-3" style="color: #f39c12;"></i>
<h4 style="margin: 0 0 10px 0;">Upgrade Plan</h4>
<p style="margin: 0; color: #a0a0a0; font-size: 14px;">View all available plans</p>
</div>
</div>
</a>
<a href="{{ url_for(request, '/dashboard/profile') }}" style="text-decoration: none; color: inherit;">
<div style="border: 1px solid #0f3460; border-radius: 5px; padding: 20px; background: #1a1a2e; transition: all 0.2s ease; cursor: pointer;" onmouseover="this.style.borderColor='#17a2b8'; this.style.boxShadow='0 2px 8px rgba(23,162,184,0.2)';" onmouseout="this.style.borderColor='#0f3460'; this.style.boxShadow='none';">
<div style="text-align: center;">
<i class="fas fa-user fa-3x mb-3" style="color: #17a2b8;"></i>
<h4 style="margin: 0 0 10px 0;">Edit Profile</h4>
<p style="margin: 0; color: #a0a0a0; font-size: 14px;">Update account settings</p>
</div>
</div>
</a>
<a href="{{ url_for(request, '/dashboard/change-password') }}" style="text-decoration: none; color: inherit;">
<div style="border: 1px solid #0f3460; border-radius: 5px; padding: 20px; background: #1a1a2e; transition: all 0.2s ease; cursor: pointer;" onmouseover="this.style.borderColor='#6c757d'; this.style.boxShadow='0 2px 8px rgba(108,117,125,0.2)';" onmouseout="this.style.borderColor='#0f3460'; this.style.boxShadow='none';">
<div style="text-align: center;">
<i class="fas fa-lock fa-3x mb-3" style="color: #6c757d;"></i>
<h4 style="margin: 0 0 10px 0;">Change Password</h4>
<p style="margin: 0; color: #a0a0a0; font-size: 14px;">Update security settings</p>
</div>
</div>
</a>
</div>
</div>
<!-- Add Payment Method Modal -->
<div class="modal fade" id="addPaymentMethodModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Add Payment Method</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<ul class="nav nav-pills mb-3 justify-content-center" id="paymentTabs">
{% if 'stripe' in enabled_gateways %}
<li class="nav-item">
<button class="nav-link active" data-bs-toggle="pill" data-bs-target="#stripeTab">
<i class="fab fa-cc-stripe me-2"></i>Credit Card
</button>
</li>
{% endif %}
{% if 'paypal' in enabled_gateways %}
<li class="nav-item">
<button class="nav-link {% if 'stripe' not in enabled_gateways %}active{% endif %}" data-bs-toggle="pill" data-bs-target="#paypalTab">
<i class="fab fa-paypal me-2"></i>PayPal
</button>
</li>
{% endif %}
{% if 'bitcoin' in enabled_gateways or 'eth' in enabled_gateways or 'usdt' in enabled_gateways or 'usdc' in enabled_gateways %}
<li class="nav-item">
<button class="nav-link" data-bs-toggle="pill" data-bs-target="#cryptoTab">
<i class="fab fa-bitcoin me-2"></i>Crypto
</button>
</li>
{% endif %}
</ul>
<div class="tab-content">
<div class="tab-pane fade show active" id="stripeTab">
<div class="text-center py-4">
<i class="fab fa-cc-stripe fa-3x mb-3 text-primary"></i>
<p>Secure credit card payment via Stripe</p>
<button class="btn btn-primary">Connect with Stripe</button>
</div>
</div>
<div class="tab-pane fade" id="paypalTab">
<div class="text-center py-4">
<i class="fab fa-paypal fa-3x mb-3 text-primary"></i>
<p>Pay with your PayPal account</p>
<button class="btn btn-primary">Connect with PayPal</button>
</div>
</div>
<div class="tab-pane fade" id="cryptoTab">
<div class="text-center py-3">
<div class="d-flex justify-content-center gap-3 mb-3">
{% if 'bitcoin' in enabled_gateways %}
<button class="btn btn-outline-warning">
<i class="fab fa-bitcoin me-2"></i>Bitcoin
</button>
{% endif %}
{% if 'eth' in enabled_gateways %}
<button class="btn btn-outline-purple">
<i class="fab fa-ethereum me-2"></i>Ethereum
</button>
{% endif %}
{% if 'usdt' in enabled_gateways %}
<button class="btn btn-outline-success">
<i class="fas fa-coins me-2"></i>USDT
</button>
{% endif %}
{% if 'usdc' in enabled_gateways %}
<button class="btn btn-outline-info">
<i class="fas fa-coins me-2"></i>USDC
</button>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_css %}
<style>
.subscription-status-active {
background: #28a745;
color: white;
padding: 4px 12px;
border-radius: 15px;
font-size: 12px;
font-weight: bold;
}
.subscription-status-canceled {
background: #ffc107;
color: black;
padding: 4px 12px;
border-radius: 15px;
font-size: 12px;
font-weight: bold;
}
.subscription-status-expired,
.subscription-status-suspended {
background: #dc3545;
color: white;
padding: 4px 12px;
border-radius: 15px;
font-size: 12px;
font-weight: bold;
}
.text-purple {
color: #6f42c1;
}
</style>
{% endblock %}
......@@ -35,15 +35,19 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<form method="POST" action="{{ url_for(request, '/dashboard/users/add') }}" style="display: flex; gap: 10px; flex-wrap: wrap; align-items: flex-end;">
<div class="form-group" style="flex: 1; min-width: 200px; margin-bottom: 0;">
<label for="username">Username</label>
<input type="text" id="username" name="username" required style="width: 100%;">
<input type="text" id="username" name="username" required style="width: 100%; background: #1a1a2e !important; color: #e0e0e0; border: 1px solid #0f3460; padding: 8px; border-radius: 3px;">
</div>
<div class="form-group" style="flex: 1; min-width: 200px; margin-bottom: 0;">
<label for="email">Email</label>
<input type="email" id="email" name="email" style="width: 100%; background: #1a1a2e !important; color: #e0e0e0; border: 1px solid #0f3460; padding: 8px; border-radius: 3px;">
</div>
<div class="form-group" style="flex: 1; min-width: 200px; margin-bottom: 0;">
<label for="password">Password</label>
<input type="password" id="password" name="password" required style="width: 100%;">
<input type="password" id="password" name="password" required style="width: 100%; background: #1a1a2e !important; color: #e0e0e0; border: 1px solid #0f3460; padding: 8px; border-radius: 3px;">
</div>
<div class="form-group" style="flex: 0 0 150px; margin-bottom: 0;">
<label for="role">Role</label>
<select id="role" name="role" style="width: 100%;">
<select id="role" name="role" style="width: 100%; background: #1a1a2e !important; color: #e0e0e0; border: 1px solid #0f3460; padding: 8px; border-radius: 3px;">
<option value="user">User</option>
<option value="admin">Admin</option>
</select>
......@@ -59,6 +63,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<tr>
<th>ID</th>
<th>Username</th>
<th>Email</th>
<th>Role</th>
<th>Created By</th>
<th>Created At</th>
......@@ -73,6 +78,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<tr>
<td>{{ user.id }}</td>
<td>{{ user.username }}</td>
<td>{{ user.email or '-' }}</td>
<td>
{% if user.role == 'admin' %}
<span style="color: #e94560; font-weight: bold;">Admin</span>
......@@ -92,7 +98,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</td>
<td>
<div style="display: flex; gap: 5px; flex-wrap: wrap;">
<button onclick="editUser({{ user.id }}, '{{ user.username }}', '{{ user.role }}', {{ user.is_active|lower }})" class="btn btn-secondary" style="padding: 5px 10px; font-size: 12px; margin: 0;">Edit</button>
<button onclick="editUser({{ user.id }}, '{{ user.username }}', '{{ user.email }}', '{{ user.role }}', {{ user.is_active|lower }})" class="btn btn-secondary" style="padding: 5px 10px; font-size: 12px; margin: 0;">Edit</button>
<button onclick="toggleUserStatus({{ user.id }}, {{ user.is_active|lower }})" class="btn btn-warning" style="padding: 5px 10px; font-size: 12px; margin: 0;">
{% if user.is_active %}Disable{% else %}Enable{% endif %}
</button>
......@@ -117,11 +123,15 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<input type="hidden" id="edit-user-id" name="user_id">
<div class="form-group">
<label for="edit-username">Username</label>
<input type="text" id="edit-username" name="username" required>
<input type="text" id="edit-username" name="username" required style="background: #1a1a2e; color: #e0e0e0; border: 1px solid #0f3460; padding: 8px; border-radius: 3px; width: 100%;">
</div>
<div class="form-group">
<label for="edit-email">Email</label>
<input type="email" id="edit-email" name="email" style="background: #1a1a2e; color: #e0e0e0; border: 1px solid #0f3460; padding: 8px; border-radius: 3px; width: 100%;">
</div>
<div class="form-group">
<label for="edit-password">New Password (leave blank to keep current)</label>
<input type="password" id="edit-password" name="password" placeholder="Enter new password">
<input type="password" id="edit-password" name="password" placeholder="Enter new password" style="background: #1a1a2e; color: #e0e0e0; border: 1px solid #0f3460; padding: 8px; border-radius: 3px; width: 100%;">
</div>
<div class="form-group">
<label for="edit-role">Role</label>
......@@ -145,9 +155,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</div>
<script>
function editUser(userId, username, role, isActive) {
function editUser(userId, username, email, role, isActive) {
document.getElementById('edit-user-id').value = userId;
document.getElementById('edit-username').value = username;
document.getElementById('edit-email').value = email;
document.getElementById('edit-role').value = role;
document.getElementById('edit-is-active').checked = isActive;
document.getElementById('edit-form').action = '{{ url_for(request, "/dashboard/users/") }}' + userId + '/edit';
......@@ -222,5 +233,13 @@ th {
background: #0f3460;
font-weight: 600;
}
/* Prevent browser autofill from overriding dark theme */
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
input:-webkit-autofill:active {
-webkit-box-shadow: 0 0 0 30px #1a1a2e inset !important;
-webkit-text-fill-color: #e0e0e0 !important;
}
</style>
{% endblock %}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment