Refactor backend and add registry build workflow
Build and publish Docker image / build-and-publish (push) Successful in 2m45s

This commit is contained in:
wfalkowski committed 2026-10-20 16:44:49 +00:00
commit c177d7981d
43 files changed
+18124

No files matched your search

+572
View File
@@ -0,0 +1,572 @@
"""Database initialization and connection helpers."""
from contextlib import contextmanager
from datetime import date, datetime, timezone
from db_wrapper import get_db_connection
import json
from runtime_config import (
DATABASE_URL,
logger,
)
__all__ = ['check_and_fix_database_schema', 'database_connection', 'dict_from_row', 'get_table_columns', 'init_database']
def init_database():
"""Initialize the database with required tables"""
try:
with database_connection() as conn:
cursor = conn.cursor()
# Users table - EXTENDED
cursor.execute('''
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
username TEXT UNIQUE NOT NULL,
email TEXT UNIQUE NOT NULL,
password_hash TEXT,
google_id TEXT UNIQUE,
apple_id TEXT UNIQUE,
role TEXT DEFAULT 'user',
credits INTEGER DEFAULT 100,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
display_name TEXT,
first_name TEXT,
last_name TEXT,
address TEXT,
city TEXT,
country TEXT,
postal_code TEXT,
phone TEXT,
email_verified BOOLEAN DEFAULT FALSE,
email_verification_token TEXT,
password_changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
two_factor_enabled BOOLEAN DEFAULT FALSE,
two_factor_secret TEXT,
last_login TIMESTAMP,
login_count INTEGER DEFAULT 0
)
''')
# Add google_id and apple_id columns if they don't exist (migration for existing databases)
if 'postgres' in DATABASE_URL:
cursor.execute('ALTER TABLE users ADD COLUMN IF NOT EXISTS google_id TEXT UNIQUE')
cursor.execute('ALTER TABLE users ADD COLUMN IF NOT EXISTS apple_id TEXT UNIQUE')
else:
try:
cursor.execute('ALTER TABLE users ADD COLUMN google_id TEXT UNIQUE')
except:
pass
try:
cursor.execute('ALTER TABLE users ADD COLUMN apple_id TEXT UNIQUE')
except:
pass
# Make password_hash nullable if it's not already (migration for existing databases)
try:
# SQLite doesn't support ALTER COLUMN, so we'll handle NULL passwords in code
pass
except:
pass
# Email verification tokens table
cursor.execute('''
CREATE TABLE IF NOT EXISTS email_verification_tokens (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
token TEXT NOT NULL,
expires_at TIMESTAMP NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
)
''')
# Password reset tokens table
cursor.execute('''
CREATE TABLE IF NOT EXISTS password_reset_tokens (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
token TEXT NOT NULL,
expires_at TIMESTAMP NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
)
''')
# Servers table
cursor.execute('''
CREATE TABLE IF NOT EXISTS servers (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
type TEXT NOT NULL,
image TEXT NOT NULL,
version TEXT NOT NULL,
status TEXT DEFAULT 'Creating',
cpu TEXT NOT NULL,
ram TEXT NOT NULL,
storage TEXT NOT NULL,
ip TEXT NOT NULL,
ports TEXT NOT NULL,
owner_id TEXT NOT NULL,
namespace TEXT NOT NULL,
k8s_name TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
is_demo BOOLEAN DEFAULT FALSE,
demo_expires_at TIMESTAMP,
java_version TEXT,
loader TEXT,
sftp_username TEXT,
sftp_password TEXT,
sftp_port INTEGER,
FOREIGN KEY (owner_id) REFERENCES users (id) ON DELETE CASCADE
)
''')
# Add demo columns if they don't exist (for existing databases)
if 'postgres' in DATABASE_URL:
cursor.execute('ALTER TABLE servers ADD COLUMN IF NOT EXISTS is_demo BOOLEAN DEFAULT FALSE')
cursor.execute('ALTER TABLE servers ADD COLUMN IF NOT EXISTS demo_expires_at TIMESTAMP')
cursor.execute('ALTER TABLE servers ADD COLUMN IF NOT EXISTS java_version TEXT')
cursor.execute('ALTER TABLE servers ADD COLUMN IF NOT EXISTS loader TEXT')
cursor.execute('ALTER TABLE servers ADD COLUMN IF NOT EXISTS sftp_username TEXT')
cursor.execute('ALTER TABLE servers ADD COLUMN IF NOT EXISTS sftp_password TEXT')
cursor.execute('ALTER TABLE servers ADD COLUMN IF NOT EXISTS sftp_port INTEGER')
else:
try:
cursor.execute('ALTER TABLE servers ADD COLUMN is_demo BOOLEAN DEFAULT FALSE')
except:
pass
try:
cursor.execute('ALTER TABLE servers ADD COLUMN demo_expires_at TIMESTAMP')
except:
pass
try:
cursor.execute('ALTER TABLE servers ADD COLUMN java_version TEXT')
except:
pass
try:
cursor.execute('ALTER TABLE servers ADD COLUMN loader TEXT')
except:
pass
try:
cursor.execute('ALTER TABLE servers ADD COLUMN sftp_username TEXT')
except:
pass
try:
cursor.execute('ALTER TABLE servers ADD COLUMN sftp_password TEXT')
except:
pass
try:
cursor.execute('ALTER TABLE servers ADD COLUMN sftp_port INTEGER')
except:
pass
# Demo servers usage tracking table
cursor.execute('''
CREATE TABLE IF NOT EXISTS demo_servers_used (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL UNIQUE,
used_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
)
''')
# Server status history table
cursor.execute('''
CREATE TABLE IF NOT EXISTS server_status_history (
id TEXT PRIMARY KEY,
server_id TEXT NOT NULL,
status TEXT NOT NULL,
message TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (server_id) REFERENCES servers (id) ON DELETE CASCADE
)
''')
# Sessions table
cursor.execute('''
CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
token TEXT NOT NULL,
expires_at TIMESTAMP NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
)
''')
# Credit transactions table
cursor.execute('''
CREATE TABLE IF NOT EXISTS credit_transactions (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
amount INTEGER NOT NULL,
type TEXT NOT NULL,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
)
''')
# Database configurations table
cursor.execute('''
CREATE TABLE IF NOT EXISTS database_configs (
id TEXT PRIMARY KEY,
server_id TEXT NOT NULL,
username TEXT NOT NULL,
password TEXT NOT NULL,
database_name TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (server_id) REFERENCES servers (id) ON DELETE CASCADE
)
''')
# Tickets table
cursor.execute('''
CREATE TABLE IF NOT EXISTS tickets (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
subject TEXT NOT NULL,
description TEXT NOT NULL,
status TEXT DEFAULT 'open',
priority TEXT DEFAULT 'medium',
category TEXT DEFAULT 'general',
assigned_admin_id TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
FOREIGN KEY (assigned_admin_id) REFERENCES users (id) ON DELETE SET NULL
)
''')
# Backups table
cursor.execute('''
CREATE TABLE IF NOT EXISTS backups (
id TEXT PRIMARY KEY,
server_id TEXT NOT NULL,
name TEXT NOT NULL,
type TEXT NOT NULL, -- 'Manual', 'Scheduled'
status TEXT DEFAULT 'Pending', -- 'Pending', 'In Progress', 'Completed', 'Failed'
size TEXT,
s3_path TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
completed_at TIMESTAMP,
FOREIGN KEY (server_id) REFERENCES servers (id) ON DELETE CASCADE
)
''')
# Ticket messages table
cursor.execute('''
CREATE TABLE IF NOT EXISTS ticket_messages (
id TEXT PRIMARY KEY,
ticket_id TEXT NOT NULL,
user_id TEXT NOT NULL,
message TEXT NOT NULL,
is_admin BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (ticket_id) REFERENCES tickets (id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
)
''')
# Ticket attachments table
# Determine blob type based on database URL
blob_type = 'BYTEA' if 'postgres' in DATABASE_URL else 'BLOB'
cursor.execute(f'''
CREATE TABLE IF NOT EXISTS ticket_attachments (
id TEXT PRIMARY KEY,
ticket_id TEXT NOT NULL,
message_id TEXT,
filename TEXT NOT NULL,
file_data {blob_type} NOT NULL,
mime_type TEXT NOT NULL,
file_size INTEGER NOT NULL,
uploaded_by TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (ticket_id) REFERENCES tickets (id) ON DELETE CASCADE,
FOREIGN KEY (message_id) REFERENCES ticket_messages (id) ON DELETE CASCADE,
FOREIGN KEY (uploaded_by) REFERENCES users (id) ON DELETE CASCADE
)
''')
# Announcements table
cursor.execute('''
CREATE TABLE IF NOT EXISTS announcements (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
content TEXT NOT NULL,
tags TEXT,
author_id TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (author_id) REFERENCES users (id) ON DELETE CASCADE
)
''')
# Key-value settings (e.g. homepage showcase URLs)
cursor.execute('''
CREATE TABLE IF NOT EXISTS site_settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
# Default pricing and panel settings
default_settings = [
('price_cpu', '35'),
('price_ram', '35'),
('price_storage', '5'),
('multiplier_vps', '1.0'),
('multiplier_game', '0.8'),
('multiplier_website', '1.5'),
('multiplier_database', '1.3'),
('demo_servers_enabled', 'true'),
('new_user_credits', '0')
]
for key, val in default_settings:
if 'postgres' in DATABASE_URL:
cursor.execute("INSERT INTO site_settings (key, value) VALUES (?, ?) ON CONFLICT (key) DO NOTHING", (key, val))
else:
cursor.execute("INSERT OR IGNORE INTO site_settings (key, value) VALUES (?, ?)", (key, val))
# User domains table - WITH is_custom column
cursor.execute('''
CREATE TABLE IF NOT EXISTS user_domains (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
subdomain TEXT,
full_domain TEXT,
domain TEXT,
is_custom BOOLEAN DEFAULT FALSE,
verified BOOLEAN DEFAULT FALSE,
record_type TEXT DEFAULT 'A',
ip_address TEXT NOT NULL,
ttl INTEGER DEFAULT 3600,
server_id TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
FOREIGN KEY (server_id) REFERENCES servers (id) ON DELETE SET NULL,
UNIQUE(user_id, full_domain),
UNIQUE(user_id, domain)
)
''')
conn.commit()
# Create indexes - moved outside transaction for safety
try:
cursor.execute('CREATE INDEX IF NOT EXISTS idx_servers_owner_id ON servers(owner_id)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_servers_status ON servers(status)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_server_status_history_server_id ON server_status_history(server_id)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_sessions_token ON sessions(token)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_credit_transactions_user_id ON credit_transactions(user_id)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_database_configs_server_id ON database_configs(server_id)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_tickets_user_id ON tickets(user_id)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_tickets_status ON tickets(status)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_tickets_priority ON tickets(priority)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_ticket_messages_ticket_id ON ticket_messages(ticket_id)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_ticket_attachments_ticket_id ON ticket_attachments(ticket_id)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_user_domains_user_id ON user_domains(user_id)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_user_domains_server_id ON user_domains(server_id)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_user_domains_full_domain ON user_domains(full_domain)')
# Check if is_custom column exists before creating index
columns = get_table_columns(cursor, 'user_domains')
has_is_custom = 'is_custom' in columns
if has_is_custom:
cursor.execute('CREATE INDEX IF NOT EXISTS idx_user_domains_is_custom ON user_domains(is_custom)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_user_domains_domain ON user_domains(domain)')
else:
logger.warning("is_custom column not found in user_domains table, skipping index creation")
conn.commit()
except Exception as e:
logger.error(f"Error creating indexes: {e}")
conn.rollback()
# Try to add missing columns
try:
columns = get_table_columns(cursor, 'user_domains')
if 'is_custom' not in columns:
logger.info("Adding missing column: is_custom to user_domains table")
cursor.execute('ALTER TABLE user_domains ADD COLUMN is_custom BOOLEAN DEFAULT FALSE')
if 'domain' not in columns:
logger.info("Adding missing column: domain to user_domains table")
cursor.execute('ALTER TABLE user_domains ADD COLUMN domain TEXT')
conn.commit()
# Now try creating indexes again
cursor.execute('CREATE INDEX IF NOT EXISTS idx_user_domains_is_custom ON user_domains(is_custom)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_user_domains_domain ON user_domains(domain)')
conn.commit()
except Exception as alter_error:
logger.error(f"Error adding missing columns: {alter_error}")
# Default rows for homepage showcase (3 configurable links)
try:
cursor.execute("SELECT 1 FROM site_settings WHERE key = ?", ('homepage_showcase',))
if not cursor.fetchone():
default_links = json.dumps([
{"label": "Site 1", "url": ""},
{"label": "Site 2", "url": ""},
{"label": "Site 3", "url": ""},
])
cursor.execute(
"INSERT INTO site_settings (key, value) VALUES (?, ?)",
("homepage_showcase", default_links),
)
conn.commit()
except Exception as seed_err:
logger.warning(f"homepage_showcase default seed: {seed_err}")
logger.info("Database initialized successfully")
except Exception as e:
logger.error(f"Error initializing database: {e}")
raise
def check_and_fix_database_schema():
"""Check and fix any missing columns in existing tables"""
from server_provisioning import generate_sftp_credentials
try:
with database_connection() as conn:
cursor = conn.cursor()
# Check user_domains table for missing columns
columns = get_table_columns(cursor, 'user_domains')
missing_columns = []
if 'is_custom' not in columns:
missing_columns.append('is_custom')
if 'domain' not in columns:
missing_columns.append('domain')
if 'verified' not in columns:
missing_columns.append('verified')
if 'java_version' not in columns:
missing_columns.append('java_version')
if 'loader' not in columns:
missing_columns.append('loader')
if missing_columns:
logger.info(f"Adding missing columns to user_domains table: {missing_columns}")
if 'is_custom' in missing_columns:
cursor.execute('ALTER TABLE user_domains ADD COLUMN is_custom BOOLEAN DEFAULT FALSE')
if 'domain' in missing_columns:
cursor.execute('ALTER TABLE user_domains ADD COLUMN domain TEXT')
if 'verified' in missing_columns:
cursor.execute('ALTER TABLE user_domains ADD COLUMN verified BOOLEAN DEFAULT FALSE')
# Check servers table for new columns
server_columns = get_table_columns(cursor, 'servers')
if 'java_version' not in server_columns:
logger.info("Adding missing column: java_version to servers table")
cursor.execute('ALTER TABLE servers ADD COLUMN java_version TEXT')
if 'loader' not in server_columns:
logger.info("Adding missing column: loader to servers table")
cursor.execute('ALTER TABLE servers ADD COLUMN loader TEXT')
if 'sftp_username' not in server_columns:
logger.info("Adding missing column: sftp_username to servers table")
cursor.execute('ALTER TABLE servers ADD COLUMN sftp_username TEXT')
if 'sftp_password' not in server_columns:
logger.info("Adding missing column: sftp_password to servers table")
cursor.execute('ALTER TABLE servers ADD COLUMN sftp_password TEXT')
if 'sftp_port' not in server_columns:
logger.info("Adding missing column: sftp_port to servers table")
cursor.execute('ALTER TABLE servers ADD COLUMN sftp_port INTEGER')
conn.commit()
logger.info("✓ Added missing columns to user_domains and servers table")
# Create indexes that might be missing
try:
cursor.execute('CREATE INDEX IF NOT EXISTS idx_user_domains_is_custom ON user_domains(is_custom)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_user_domains_domain ON user_domains(domain)')
conn.commit()
except Exception as index_error:
logger.warning(f"Could not create indexes: {index_error}")
# Populate missing SFTP credentials for existing servers
try:
cursor.execute("SELECT id, type FROM servers WHERE sftp_username IS NULL OR sftp_username = ''")
servers_to_fix = cursor.fetchall()
if servers_to_fix:
logger.info(f"Populating SFTP credentials for {len(servers_to_fix)} servers")
for row in servers_to_fix:
srv = dict_from_row(row)
if srv['type'] != 'vps':
u, p, port = generate_sftp_credentials()
cursor.execute(
'UPDATE servers SET sftp_username = ?, sftp_password = ?, sftp_port = ? WHERE id = ?',
(u, p, port, srv['id'])
)
conn.commit()
logger.info("✓ Populated missing SFTP credentials")
except Exception as sftp_fix_error:
logger.warning(f"Could not populate SFTP credentials: {sftp_fix_error}")
except Exception as e:
logger.error(f"Error checking database schema: {e}")
@contextmanager
def database_connection():
"""Context manager for database connections"""
conn = get_db_connection(DATABASE_URL)
# Row factory is handled in get_db_connection for SQLite, and via cursor_factory for Postgres
try:
yield conn
except Exception as e:
conn.rollback()
raise e
finally:
conn.close()
def dict_from_row(row):
"""Convert sqlite3.Row or dict to dictionary, making naive datetimes UTC-aware."""
if row is None:
return None
result = dict(row)
for key, value in result.items():
if isinstance(value, datetime):
# If datetime is naive, assume it's UTC and make it aware.
if value.tzinfo is None:
value = value.replace(tzinfo=timezone.utc)
result[key] = value.isoformat()
elif isinstance(value, date):
result[key] = value.isoformat()
return result
def get_table_columns(cursor, table_name):
"""Get column names for a table in a database-agnostic way"""
if 'postgres' in DATABASE_URL:
cursor.execute("SELECT column_name FROM information_schema.columns WHERE table_name = ?", (table_name,))
return [row['column_name'] for row in cursor.fetchall()]
else:
cursor.execute(f"PRAGMA table_info({table_name})")
return [col[1] for col in cursor.fetchall()]