Files
appfactory-portal/app/db/migrations.py
T
2026-07-30 07:13:12 +02:00

313 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from app.db.database import get_connection
def _table_columns(con, table_name: str) -> set[str]:
rows = con.execute(f"PRAGMA table_info({table_name})").fetchall()
return {row["name"] for row in rows}
def ensure_user_columns(con) -> None:
con.execute(
"""
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
display_name TEXT,
email TEXT,
password_hash TEXT NOT NULL DEFAULT '',
role TEXT NOT NULL DEFAULT 'viewer',
is_active INTEGER NOT NULL DEFAULT 1,
is_enabled INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
last_login_at TEXT,
auth_provider TEXT,
provider_subject TEXT,
avatar_url TEXT,
gitea_user_id INTEGER,
gitea_username TEXT,
gitea_sync_status TEXT NOT NULL DEFAULT 'not_required',
gitea_sync_error TEXT,
gitea_synced_at TEXT
)
"""
)
columns = _table_columns(con, "users")
if not columns:
return
defaults = {
"username": "TEXT",
"display_name": "TEXT",
"email": "TEXT",
"password_hash": "TEXT NOT NULL DEFAULT ''",
"role": "TEXT NOT NULL DEFAULT 'viewer'",
"is_active": "INTEGER NOT NULL DEFAULT 1",
"is_enabled": "INTEGER NOT NULL DEFAULT 1",
"created_at": "TEXT",
"updated_at": "TEXT",
"last_login_at": "TEXT",
"auth_provider": "TEXT",
"provider_subject": "TEXT",
"avatar_url": "TEXT",
"gitea_user_id": "INTEGER",
"gitea_username": "TEXT",
"gitea_sync_status": "TEXT NOT NULL DEFAULT 'not_required'",
"gitea_sync_error": "TEXT",
"gitea_synced_at": "TEXT",
}
for column, definition in defaults.items():
if column not in columns:
con.execute(f"ALTER TABLE users ADD COLUMN {column} {definition}")
columns = _table_columns(con, "users")
if "is_active" in columns and "is_enabled" in columns:
con.execute("UPDATE users SET is_enabled = COALESCE(is_active, 1) WHERE is_enabled IS NULL")
con.execute("UPDATE users SET role = 'viewer' WHERE role IS NULL OR role = ''")
con.execute("UPDATE users SET gitea_sync_status = 'not_required' WHERE gitea_sync_status IS NULL OR gitea_sync_status = ''")
con.execute("UPDATE users SET created_at = CURRENT_TIMESTAMP WHERE created_at IS NULL OR created_at = ''")
con.execute("UPDATE users SET updated_at = COALESCE(created_at, CURRENT_TIMESTAMP) WHERE updated_at IS NULL OR updated_at = ''")
con.execute("CREATE INDEX IF NOT EXISTS idx_users_email ON users(email)")
con.execute("CREATE INDEX IF NOT EXISTS idx_users_provider_subject ON users(auth_provider, provider_subject)")
def run_migrations():
con = get_connection()
ensure_user_columns(con)
con.execute(
"""
CREATE TABLE IF NOT EXISTS jobs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
type TEXT NOT NULL,
target_type TEXT NOT NULL,
target_id TEXT NOT NULL,
payload_json TEXT NOT NULL DEFAULT '{}',
status TEXT NOT NULL DEFAULT 'queued',
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
started_at TEXT,
finished_at TEXT,
created_by_user_id INTEGER,
created_by_username TEXT,
created_by_display_name TEXT,
source TEXT NOT NULL DEFAULT 'portal',
worker_id TEXT,
result_json TEXT,
error_text TEXT
)
"""
)
con.execute(
"""
CREATE TABLE IF NOT EXISTS job_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
job_id INTEGER NOT NULL,
stream TEXT NOT NULL,
message TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(job_id) REFERENCES jobs(id)
)
"""
)
con.execute(
"""
CREATE TABLE IF NOT EXISTS workers (
id TEXT PRIMARY KEY,
status TEXT NOT NULL DEFAULT 'online',
started_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
last_seen_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
current_job_id INTEGER,
metadata_json TEXT
)
"""
)
con.execute("CREATE INDEX IF NOT EXISTS idx_jobs_status_created ON jobs(status, created_at)")
con.execute("CREATE INDEX IF NOT EXISTS idx_jobs_target_status ON jobs(target_type, target_id, status)")
con.execute("CREATE INDEX IF NOT EXISTS idx_job_logs_job_id ON job_logs(job_id, id)")
con.execute("CREATE INDEX IF NOT EXISTS idx_workers_last_seen ON workers(last_seen_at)")
try:
con.execute("ALTER TABLE jobs ADD COLUMN created_by_display_name TEXT")
except Exception:
pass
try:
con.execute("ALTER TABLE deployments ADD COLUMN job_id INTEGER")
except Exception:
pass
for statement in (
"ALTER TABLE apps ADD COLUMN description TEXT",
"ALTER TABLE apps ADD COLUMN owner TEXT",
"ALTER TABLE apps ADD COLUMN template TEXT",
"ALTER TABLE apps ADD COLUMN runtime TEXT",
"ALTER TABLE apps ADD COLUMN repository_url TEXT",
"ALTER TABLE apps ADD COLUMN repository_name TEXT",
"ALTER TABLE apps ADD COLUMN default_branch TEXT",
"ALTER TABLE apps ADD COLUMN domain TEXT",
"ALTER TABLE apps ADD COLUMN health_url TEXT",
"ALTER TABLE apps ADD COLUMN container_port INTEGER",
"ALTER TABLE apps ADD COLUMN is_public INTEGER NOT NULL DEFAULT 0",
"ALTER TABLE apps ADD COLUMN is_enabled INTEGER NOT NULL DEFAULT 1",
):
try:
con.execute(statement)
except Exception:
pass
con.execute(
"""
CREATE TABLE IF NOT EXISTS app_templates (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
runtime TEXT,
language TEXT,
description TEXT,
default_port INTEGER,
default_health_path TEXT,
create_script TEXT,
deploy_script TEXT,
is_enabled INTEGER NOT NULL DEFAULT 1,
create_enabled INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
)
"""
)
con.execute(
"""
CREATE TABLE IF NOT EXISTS app_variables (
id INTEGER PRIMARY KEY AUTOINCREMENT,
app_id TEXT NOT NULL,
"key" TEXT NOT NULL,
value TEXT,
is_secret INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(app_id) REFERENCES apps(id)
)
"""
)
con.execute(
"""
CREATE TABLE IF NOT EXISTS service_incidents (
id INTEGER PRIMARY KEY AUTOINCREMENT,
service_id TEXT NOT NULL,
status TEXT NOT NULL,
started_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
ended_at TEXT,
duration_seconds INTEGER,
title TEXT,
description TEXT
)
"""
)
for statement in (
"ALTER TABLE app_templates ADD COLUMN id TEXT",
"ALTER TABLE app_templates ADD COLUMN name TEXT",
"ALTER TABLE app_templates ADD COLUMN runtime TEXT",
"ALTER TABLE app_templates ADD COLUMN language TEXT",
"ALTER TABLE app_templates ADD COLUMN description TEXT",
"ALTER TABLE app_templates ADD COLUMN default_port INTEGER",
"ALTER TABLE app_templates ADD COLUMN default_health_path TEXT",
"ALTER TABLE app_templates ADD COLUMN create_script TEXT",
"ALTER TABLE app_templates ADD COLUMN deploy_script TEXT",
"ALTER TABLE app_templates ADD COLUMN is_enabled INTEGER NOT NULL DEFAULT 1",
"ALTER TABLE app_templates ADD COLUMN create_enabled INTEGER NOT NULL DEFAULT 0",
"ALTER TABLE app_templates ADD COLUMN created_at TEXT",
"ALTER TABLE app_templates ADD COLUMN updated_at TEXT",
"ALTER TABLE app_variables ADD COLUMN created_at TEXT",
"ALTER TABLE app_variables ADD COLUMN updated_at TEXT",
):
try:
con.execute(statement)
except Exception:
pass
try:
con.execute("UPDATE app_templates SET id = name WHERE id IS NULL OR id = ''")
except Exception:
pass
con.execute("CREATE INDEX IF NOT EXISTS idx_app_variables_app_id ON app_variables(app_id)")
con.execute("CREATE INDEX IF NOT EXISTS idx_service_incidents_status_started ON service_incidents(status, started_at)")
con.execute("CREATE INDEX IF NOT EXISTS idx_service_incidents_service_started ON service_incidents(service_id, started_at)")
con.execute(
"""
CREATE TABLE IF NOT EXISTS alert_rules (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
description TEXT,
event_type TEXT NOT NULL,
service_id TEXT,
script_name TEXT NOT NULL,
is_enabled INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
)
"""
)
con.execute(
"""
CREATE TABLE IF NOT EXISTS alert_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
rule_id INTEGER,
event_type TEXT NOT NULL,
service_id TEXT,
incident_id INTEGER,
status TEXT,
job_id INTEGER,
error_text TEXT,
payload_json TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
processed_at TEXT
)
"""
)
con.execute("CREATE INDEX IF NOT EXISTS idx_alert_rules_event_enabled ON alert_rules(event_type, is_enabled)")
con.execute("CREATE INDEX IF NOT EXISTS idx_alert_rules_service ON alert_rules(service_id)")
con.execute("CREATE INDEX IF NOT EXISTS idx_alert_events_rule_created ON alert_events(rule_id, created_at)")
con.execute("CREATE INDEX IF NOT EXISTS idx_alert_events_created ON alert_events(created_at)")
con.execute("CREATE INDEX IF NOT EXISTS idx_alert_events_job_id ON alert_events(job_id)")
con.execute(
"""
CREATE TABLE IF NOT EXISTS app_ip_access_rules (
id INTEGER PRIMARY KEY AUTOINCREMENT,
app_id TEXT NOT NULL,
ip_cidr TEXT NOT NULL,
methods TEXT NOT NULL DEFAULT 'WRITE',
description TEXT,
is_enabled INTEGER NOT NULL DEFAULT 1,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
)
"""
)
con.execute("CREATE INDEX IF NOT EXISTS idx_app_ip_access_rules_app_id ON app_ip_access_rules(app_id)")
# Pojmenovaný katalog IP adres. Jedna adresa (např. "Klient X kancelář") se přiřazuje do N
# služeb; přiřazení zůstává řádkem v app_ip_access_rules (zdroj pravdy pro generátor Caddyfile),
# jen s vazbou address_id na katalog. Ručně zadaná pravidla mají address_id = NULL.
con.execute(
"""
CREATE TABLE IF NOT EXISTS ip_addresses (
id INTEGER PRIMARY KEY AUTOINCREMENT,
label TEXT NOT NULL,
ip_cidr TEXT NOT NULL,
note TEXT,
default_methods TEXT NOT NULL DEFAULT 'WRITE',
is_enabled INTEGER NOT NULL DEFAULT 1,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
)
"""
)
con.execute("CREATE INDEX IF NOT EXISTS idx_ip_addresses_label ON ip_addresses(label)")
if "address_id" not in _table_columns(con, "app_ip_access_rules"):
con.execute("ALTER TABLE app_ip_access_rules ADD COLUMN address_id INTEGER")
con.execute("CREATE INDEX IF NOT EXISTS idx_app_ip_access_rules_address_id ON app_ip_access_rules(address_id)")
con.commit()
con.close()