import os import hashlib import base64 import secrets import mysql.connector import logging from flask import current_app # ── Connection, crypto, schema init ───────────────────────────────────────── def hash_password(password, salt=None): if salt is None: salt = base64.b64encode(os.urandom(16)).decode('utf-8') salted = (salt + password).encode('utf-8') hashed = hashlib.sha256(salted).hexdigest() return f'{salt}${hashed}' def verify_password(password, hashed): try: salt, hash_val = hashed.split('$', 1) except ValueError: return False return hash_password(password, salt) == hashed def generate_api_key(): """Generate a secure API key""" return secrets.token_urlsafe(32) def get_db_connection(app=None): if app is None: app = current_app conn = mysql.connector.connect( host=app.config['MYSQL_HOST'], user=app.config['MYSQL_USER'], password=app.config['MYSQL_PASSWORD'], database=app.config['MYSQL_DATABASE'], autocommit=True ) return conn def init_db(app=None): if app is None: app = current_app conn = get_db_connection(app) cursor = conn.cursor() cursor.execute(''' CREATE TABLE IF NOT EXISTS User ( id INTEGER PRIMARY KEY AUTO_INCREMENT, name VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL UNIQUE, password VARCHAR(255) NOT NULL ) ''') cursor.execute(''' CREATE TABLE IF NOT EXISTS Subnet ( id INTEGER PRIMARY KEY AUTO_INCREMENT, name VARCHAR(255) NOT NULL, cidr VARCHAR(255) NOT NULL, site VARCHAR(255) ) ''') cursor.execute(''' CREATE TABLE IF NOT EXISTS AuditLog ( id INTEGER PRIMARY KEY AUTO_INCREMENT, user_id INTEGER, action VARCHAR(255) NOT NULL, details TEXT, subnet_id INTEGER, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES User(id) ON DELETE SET NULL, FOREIGN KEY (subnet_id) REFERENCES Subnet(id) ON DELETE SET NULL ) ''') cursor.execute(''' CREATE TABLE IF NOT EXISTS IPAddress ( id INTEGER PRIMARY KEY AUTO_INCREMENT, ip VARCHAR(255) NOT NULL, hostname VARCHAR(255), subnet_id INTEGER NOT NULL, FOREIGN KEY (subnet_id) REFERENCES Subnet (id) ) ''') cursor.execute(''' CREATE TABLE IF NOT EXISTS Device ( id INTEGER PRIMARY KEY AUTO_INCREMENT, name VARCHAR(255) NOT NULL, description TEXT ) ''') cursor.execute(''' CREATE TABLE IF NOT EXISTS DeviceIPAddress ( id INTEGER PRIMARY KEY AUTO_INCREMENT, device_id INTEGER NOT NULL, ip_id INTEGER NOT NULL, FOREIGN KEY (device_id) REFERENCES Device (id), FOREIGN KEY (ip_id) REFERENCES IPAddress (id) ) ''') cursor.execute(''' CREATE TABLE IF NOT EXISTS DHCPPool ( id INTEGER PRIMARY KEY AUTO_INCREMENT, subnet_id INTEGER NOT NULL, start_ip VARCHAR(255) NOT NULL, end_ip VARCHAR(255) NOT NULL, excluded_ips TEXT, FOREIGN KEY (subnet_id) REFERENCES Subnet(id) ON DELETE CASCADE ) ''') cursor.execute(''' CREATE TABLE IF NOT EXISTS Rack ( id INTEGER PRIMARY KEY AUTO_INCREMENT, name VARCHAR(255) NOT NULL, site VARCHAR(255) NOT NULL, height_u INTEGER NOT NULL ) ''') cursor.execute(''' CREATE TABLE IF NOT EXISTS RackDevice ( id INTEGER PRIMARY KEY AUTO_INCREMENT, rack_id INTEGER NOT NULL, device_id INTEGER, position_u INTEGER NOT NULL, side ENUM('front', 'back') NOT NULL, nonnet_device_name VARCHAR(255), FOREIGN KEY (rack_id) REFERENCES Rack(id) ON DELETE CASCADE, FOREIGN KEY (device_id) REFERENCES Device(id) ON DELETE CASCADE ) ''') # Create Role table cursor.execute(''' CREATE TABLE IF NOT EXISTS Role ( id INTEGER PRIMARY KEY AUTO_INCREMENT, name VARCHAR(255) NOT NULL UNIQUE, description TEXT ) ''') # Create Permission table cursor.execute(''' CREATE TABLE IF NOT EXISTS Permission ( id INTEGER PRIMARY KEY AUTO_INCREMENT, name VARCHAR(255) NOT NULL UNIQUE, description TEXT, category VARCHAR(255) ) ''') # Create RolePermission junction table cursor.execute(''' CREATE TABLE IF NOT EXISTS RolePermission ( role_id INTEGER NOT NULL, permission_id INTEGER NOT NULL, PRIMARY KEY (role_id, permission_id), FOREIGN KEY (role_id) REFERENCES Role(id) ON DELETE CASCADE, FOREIGN KEY (permission_id) REFERENCES Permission(id) ON DELETE CASCADE ) ''') # Add role_id column to User table if it doesn't exist cursor.execute("SHOW COLUMNS FROM User LIKE 'role_id'") if not cursor.fetchone(): cursor.execute('ALTER TABLE User ADD COLUMN role_id INTEGER DEFAULT NULL') try: cursor.execute('ALTER TABLE User ADD CONSTRAINT fk_user_role FOREIGN KEY (role_id) REFERENCES Role(id)') except mysql.connector.Error as e: if e.errno != 1061 and e.errno != 1826 and 'Duplicate' not in str(e): raise # Add api_key column to User table if it doesn't exist cursor.execute("SHOW COLUMNS FROM User LIKE 'api_key'") if not cursor.fetchone(): cursor.execute('ALTER TABLE User ADD COLUMN api_key VARCHAR(255) DEFAULT NULL UNIQUE') # Add 2FA columns to User table if they don't exist cursor.execute("SHOW COLUMNS FROM User LIKE 'totp_secret'") if not cursor.fetchone(): cursor.execute('ALTER TABLE User ADD COLUMN totp_secret VARCHAR(255) DEFAULT NULL') cursor.execute("SHOW COLUMNS FROM User LIKE 'totp_enabled'") if not cursor.fetchone(): cursor.execute('ALTER TABLE User ADD COLUMN totp_enabled BOOLEAN DEFAULT FALSE') cursor.execute("SHOW COLUMNS FROM User LIKE 'backup_codes'") if not cursor.fetchone(): cursor.execute('ALTER TABLE User ADD COLUMN backup_codes TEXT DEFAULT NULL') cursor.execute("SHOW COLUMNS FROM User LIKE 'two_fa_setup_complete'") if not cursor.fetchone(): cursor.execute('ALTER TABLE User ADD COLUMN two_fa_setup_complete BOOLEAN DEFAULT FALSE') # Add require_2fa column to Role table if it doesn't exist cursor.execute("SHOW COLUMNS FROM Role LIKE 'require_2fa'") if not cursor.fetchone(): cursor.execute('ALTER TABLE Role ADD COLUMN require_2fa BOOLEAN DEFAULT FALSE') # Ensure AuditLog foreign keys have ON DELETE SET NULL to preserve audit logs # This is critical - audit logs should NEVER be deleted, even when referenced entities are deleted try: # Check and update user_id foreign key cursor.execute(''' SELECT CONSTRAINT_NAME FROM information_schema.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'AuditLog' AND COLUMN_NAME = 'user_id' AND REFERENCED_TABLE_NAME = 'User' ''') fk_user = cursor.fetchone() if fk_user: fk_name = fk_user[0] # Drop and recreate with ON DELETE SET NULL cursor.execute(f'ALTER TABLE AuditLog DROP FOREIGN KEY {fk_name}') cursor.execute('ALTER TABLE AuditLog ADD CONSTRAINT fk_auditlog_user FOREIGN KEY (user_id) REFERENCES User(id) ON DELETE SET NULL') except mysql.connector.Error as e: # Foreign key might not exist or already be correct, continue if e.errno != 1025 and e.errno != 1091: # Not "Cannot drop foreign key" or "Unknown key" logging.warning(f"Could not update AuditLog user_id foreign key: {e}") try: # Check and update subnet_id foreign key cursor.execute(''' SELECT CONSTRAINT_NAME FROM information_schema.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'AuditLog' AND COLUMN_NAME = 'subnet_id' AND REFERENCED_TABLE_NAME = 'Subnet' ''') fk_subnet = cursor.fetchone() if fk_subnet: fk_name = fk_subnet[0] # Drop and recreate with ON DELETE SET NULL cursor.execute(f'ALTER TABLE AuditLog DROP FOREIGN KEY {fk_name}') cursor.execute('ALTER TABLE AuditLog ADD CONSTRAINT fk_auditlog_subnet FOREIGN KEY (subnet_id) REFERENCES Subnet(id) ON DELETE SET NULL') except mysql.connector.Error as e: # Foreign key might not exist or already be correct, continue if e.errno != 1025 and e.errno != 1091: # Not "Cannot drop foreign key" or "Unknown key" logging.warning(f"Could not update AuditLog subnet_id foreign key: {e}") # Create Tag table cursor.execute(''' CREATE TABLE IF NOT EXISTS Tag ( id INTEGER PRIMARY KEY AUTO_INCREMENT, name VARCHAR(255) NOT NULL UNIQUE, color VARCHAR(7) DEFAULT '#6B7280', description TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ''') # Create DeviceTag junction table cursor.execute(''' CREATE TABLE IF NOT EXISTS DeviceTag ( id INTEGER PRIMARY KEY AUTO_INCREMENT, device_id INTEGER NOT NULL, tag_id INTEGER NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY unique_device_tag (device_id, tag_id), FOREIGN KEY (device_id) REFERENCES Device(id) ON DELETE CASCADE, FOREIGN KEY (tag_id) REFERENCES Tag(id) ON DELETE CASCADE ) ''') # Create CustomFieldDefinition table cursor.execute(''' CREATE TABLE IF NOT EXISTS CustomFieldDefinition ( id INTEGER PRIMARY KEY AUTO_INCREMENT, entity_type ENUM('device', 'subnet') NOT NULL, name VARCHAR(255) NOT NULL, field_key VARCHAR(255) NOT NULL UNIQUE, field_type VARCHAR(50) NOT NULL, required BOOLEAN DEFAULT FALSE, default_value TEXT, help_text TEXT, display_order INTEGER DEFAULT 0, validation_rules TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ''') # Add custom_fields column to Device table if it doesn't exist cursor.execute("SHOW COLUMNS FROM Device LIKE 'custom_fields'") if not cursor.fetchone(): cursor.execute('ALTER TABLE Device ADD COLUMN custom_fields TEXT DEFAULT NULL') # Initialize existing records with empty JSON object cursor.execute("UPDATE Device SET custom_fields = '{}' WHERE custom_fields IS NULL") # Add custom_fields column to Subnet table if it doesn't exist cursor.execute("SHOW COLUMNS FROM Subnet LIKE 'custom_fields'") if not cursor.fetchone(): cursor.execute('ALTER TABLE Subnet ADD COLUMN custom_fields TEXT DEFAULT NULL') # Initialize existing records with empty JSON object cursor.execute("UPDATE Subnet SET custom_fields = '{}' WHERE custom_fields IS NULL") # Add notes column to IPAddress table if it doesn't exist cursor.execute("SHOW COLUMNS FROM IPAddress LIKE 'notes'") if not cursor.fetchone(): cursor.execute('ALTER TABLE IPAddress ADD COLUMN notes TEXT DEFAULT NULL') # Add VLAN columns to Subnet table if they don't exist cursor.execute("SHOW COLUMNS FROM Subnet LIKE 'vlan_id'") if not cursor.fetchone(): cursor.execute('ALTER TABLE Subnet ADD COLUMN vlan_id INTEGER DEFAULT NULL') cursor.execute("SHOW COLUMNS FROM Subnet LIKE 'vlan_description'") if not cursor.fetchone(): cursor.execute('ALTER TABLE Subnet ADD COLUMN vlan_description VARCHAR(255) DEFAULT NULL') cursor.execute("SHOW COLUMNS FROM Subnet LIKE 'vlan_notes'") if not cursor.fetchone(): cursor.execute('ALTER TABLE Subnet ADD COLUMN vlan_notes TEXT DEFAULT NULL') # Define all permissions with categories permissions = [ # View permissions ('view_index', 'View Home/Index page', 'View'), ('view_devices', 'View Devices page', 'View'), ('view_device', 'View Device details', 'View'), ('view_subnet', 'View Subnet details', 'View'), ('view_racks', 'View Racks page', 'View'), ('view_rack', 'View Rack details', 'View'), ('view_audit', 'View Audit Log', 'View'), ('view_admin', 'View Admin panel', 'View'), ('view_users', 'View Users page', 'View'), ('view_dhcp', 'View DHCP configuration', 'View'), # Device permissions ('add_device', 'Add new device', 'Device'), ('edit_device', 'Edit device (rename, description)', 'Device'), ('delete_device', 'Delete device', 'Device'), ('add_device_ip', 'Add IP address to device', 'Device'), ('remove_device_ip', 'Remove IP address from device', 'Device'), # Subnet permissions ('add_subnet', 'Add new subnet', 'Subnet'), ('edit_subnet', 'Edit subnet (name, CIDR, site)', 'Subnet'), ('delete_subnet', 'Delete subnet', 'Subnet'), ('export_subnet_csv', 'Export subnet as CSV', 'Subnet'), # Rack permissions ('add_rack', 'Add new rack', 'Rack'), ('delete_rack', 'Delete rack', 'Rack'), ('add_device_to_rack', 'Add device to rack', 'Rack'), ('remove_device_from_rack', 'Remove device from rack', 'Rack'), ('add_nonnet_device_to_rack', 'Add non-networked device to rack', 'Rack'), ('export_rack_csv', 'Export rack as CSV', 'Rack'), # DHCP permissions ('configure_dhcp', 'Configure DHCP pools', 'DHCP'), # Tag permissions ('view_tags', 'View tags', 'Tag'), ('add_tag', 'Add new tag', 'Tag'), ('edit_tag', 'Edit tag', 'Tag'), ('delete_tag', 'Delete tag', 'Tag'), ('assign_device_tag', 'Assign tag to device', 'Tag'), ('remove_device_tag', 'Remove tag from device', 'Tag'), # Custom Fields permissions ('view_custom_fields', 'View custom fields', 'Custom Fields'), ('manage_custom_fields', 'Manage custom fields (add, edit, delete)', 'Custom Fields'), # Admin permissions ('manage_users', 'Manage users (add, edit, delete)', 'Admin'), ('manage_roles', 'Manage roles and permissions', 'Admin'), ] # Insert permissions for perm_name, perm_desc, perm_category in permissions: cursor.execute('SELECT id FROM Permission WHERE name = %s', (perm_name,)) if not cursor.fetchone(): cursor.execute('INSERT INTO Permission (name, description, category) VALUES (%s, %s, %s)', (perm_name, perm_desc, perm_category)) # Create default roles if they don't exist cursor.execute('SELECT id FROM Role WHERE name = %s', ('admin',)) admin_role = cursor.fetchone() if not admin_role: cursor.execute('INSERT INTO Role (name, description) VALUES (%s, %s)', ('admin', 'Administrator with full access to all features')) admin_role_id = cursor.lastrowid else: admin_role_id = admin_role[0] cursor.execute('SELECT id FROM Role WHERE name = %s', ('user',)) user_role = cursor.fetchone() if not user_role: cursor.execute('INSERT INTO Role (name, description) VALUES (%s, %s)', ('user', 'Standard user with access to most features except admin functions')) user_role_id = cursor.lastrowid else: user_role_id = user_role[0] cursor.execute('SELECT id FROM Role WHERE name = %s', ('view_only',)) view_only_role = cursor.fetchone() if not view_only_role: cursor.execute('INSERT INTO Role (name, description) VALUES (%s, %s)', ('view_only', 'View-only user with read-only access to all pages')) view_only_role_id = cursor.lastrowid else: view_only_role_id = view_only_role[0] # Assign all permissions to admin role cursor.execute('SELECT id FROM Permission') all_permission_ids = [row[0] for row in cursor.fetchall()] for perm_id in all_permission_ids: cursor.execute('SELECT role_id FROM RolePermission WHERE role_id = %s AND permission_id = %s', (admin_role_id, perm_id)) if not cursor.fetchone(): cursor.execute('INSERT INTO RolePermission (role_id, permission_id) VALUES (%s, %s)', (admin_role_id, perm_id)) # Assign non-admin permissions to user role non_admin_permissions = [ 'view_index', 'view_devices', 'view_device', 'view_subnet', 'view_racks', 'view_rack', 'view_audit', 'view_dhcp', 'add_device', 'edit_device', 'delete_device', 'add_device_ip', 'remove_device_ip', 'add_subnet', 'edit_subnet', 'delete_subnet', 'export_subnet_csv', 'add_rack', 'delete_rack', 'add_device_to_rack', 'remove_device_from_rack', 'add_nonnet_device_to_rack', 'export_rack_csv', 'configure_dhcp', 'view_tags', 'add_tag', 'edit_tag', 'delete_tag', 'assign_device_tag', 'remove_device_tag', 'view_custom_fields', 'manage_custom_fields' ] for perm_name in non_admin_permissions: cursor.execute('SELECT id FROM Permission WHERE name = %s', (perm_name,)) perm_result = cursor.fetchone() if perm_result: perm_id = perm_result[0] cursor.execute('SELECT role_id FROM RolePermission WHERE role_id = %s AND permission_id = %s', (user_role_id, perm_id)) if not cursor.fetchone(): cursor.execute('INSERT INTO RolePermission (role_id, permission_id) VALUES (%s, %s)', (user_role_id, perm_id)) # Assign view-only permissions to view_only role # Same view permissions as user role, but excluding admin views (view_admin, view_users) view_only_permissions = [ 'view_index', 'view_devices', 'view_device', 'view_subnet', 'view_racks', 'view_rack', 'view_audit', 'view_dhcp', 'view_tags', 'view_custom_fields' ] for perm_name in view_only_permissions: cursor.execute('SELECT id FROM Permission WHERE name = %s', (perm_name,)) perm_result = cursor.fetchone() if perm_result: perm_id = perm_result[0] cursor.execute('SELECT role_id FROM RolePermission WHERE role_id = %s AND permission_id = %s', (view_only_role_id, perm_id)) if not cursor.fetchone(): cursor.execute('INSERT INTO RolePermission (role_id, permission_id) VALUES (%s, %s)', (view_only_role_id, perm_id)) # Assign existing users to 'admin' role if they don't have a role # This ensures existing users maintain admin access cursor.execute('UPDATE User SET role_id = %s WHERE role_id IS NULL', (admin_role_id,)) # Generate API keys for users that don't have one cursor.execute('SELECT id FROM User WHERE api_key IS NULL') users_without_api_key = cursor.fetchall() for (user_id,) in users_without_api_key: api_key = generate_api_key() cursor.execute('UPDATE User SET api_key = %s WHERE id = %s', (api_key, user_id)) cursor.execute('SELECT COUNT(*) FROM User') if cursor.fetchone()[0] == 0: api_key = generate_api_key() cursor.execute('''INSERT INTO User (name, email, password, role_id, api_key) VALUES (%s, %s, %s, %s, %s)''', ('admin', 'admin@example.com', hash_password('password'), admin_role_id, api_key)) # Create indexes for performance optimization logging.info("Creating database indexes for performance...") def create_index_if_not_exists(cursor, index_name, table_name, columns): """Helper function to create index if it doesn't exist""" try: # Check if index exists cursor.execute(''' SELECT COUNT(*) FROM information_schema.statistics WHERE table_schema = DATABASE() AND table_name = %s AND index_name = %s ''', (table_name, index_name)) if cursor.fetchone()[0] == 0: cursor.execute(f'CREATE INDEX {index_name} ON {table_name}({columns})') logging.info(f"Created index {index_name}") else: logging.debug(f"Index {index_name} already exists") except mysql.connector.Error as e: logging.warning(f"Could not create index {index_name}: {e}") # IPAddress table indexes create_index_if_not_exists(cursor, 'idx_ipaddress_subnet_id', 'IPAddress', 'subnet_id') create_index_if_not_exists(cursor, 'idx_ipaddress_hostname', 'IPAddress', 'hostname') create_index_if_not_exists(cursor, 'idx_ipaddress_ip', 'IPAddress', 'ip') create_index_if_not_exists(cursor, 'idx_ipaddress_subnet_hostname', 'IPAddress', 'subnet_id, hostname') create_index_if_not_exists(cursor, 'idx_ipaddress_notes', 'IPAddress', 'notes(255)') # DeviceIPAddress table indexes create_index_if_not_exists(cursor, 'idx_deviceipaddress_device_id', 'DeviceIPAddress', 'device_id') create_index_if_not_exists(cursor, 'idx_deviceipaddress_ip_id', 'DeviceIPAddress', 'ip_id') create_index_if_not_exists(cursor, 'idx_deviceipaddress_device_ip', 'DeviceIPAddress', 'device_id, ip_id') # AuditLog table indexes create_index_if_not_exists(cursor, 'idx_auditlog_timestamp', 'AuditLog', 'timestamp') create_index_if_not_exists(cursor, 'idx_auditlog_user_id', 'AuditLog', 'user_id') create_index_if_not_exists(cursor, 'idx_auditlog_subnet_id', 'AuditLog', 'subnet_id') create_index_if_not_exists(cursor, 'idx_auditlog_action', 'AuditLog', 'action') create_index_if_not_exists(cursor, 'idx_auditlog_user_timestamp', 'AuditLog', 'user_id, timestamp') create_index_if_not_exists(cursor, 'idx_auditlog_subnet_timestamp', 'AuditLog', 'subnet_id, timestamp') # Subnet table indexes create_index_if_not_exists(cursor, 'idx_subnet_site', 'Subnet', 'site') create_index_if_not_exists(cursor, 'idx_subnet_site_name', 'Subnet', 'site, name') # DeviceTag table indexes create_index_if_not_exists(cursor, 'idx_devicetag_device_id', 'DeviceTag', 'device_id') create_index_if_not_exists(cursor, 'idx_devicetag_tag_id', 'DeviceTag', 'tag_id') # DHCPPool table indexes create_index_if_not_exists(cursor, 'idx_dhcppool_subnet_id', 'DHCPPool', 'subnet_id') # RackDevice table indexes create_index_if_not_exists(cursor, 'idx_rackdevice_rack_id', 'RackDevice', 'rack_id') create_index_if_not_exists(cursor, 'idx_rackdevice_device_id', 'RackDevice', 'device_id') create_index_if_not_exists(cursor, 'idx_rackdevice_rack_side', 'RackDevice', 'rack_id, side') # Device table indexes # User table indexes (api_key already has UNIQUE index) create_index_if_not_exists(cursor, 'idx_user_role_id', 'User', 'role_id') # CustomFieldDefinition table indexes create_index_if_not_exists(cursor, 'idx_customfield_entity_type', 'CustomFieldDefinition', 'entity_type') create_index_if_not_exists(cursor, 'idx_customfield_field_key', 'CustomFieldDefinition', 'field_key') create_index_if_not_exists(cursor, 'idx_customfield_entity_order', 'CustomFieldDefinition', 'entity_type, display_order') logging.info("Database indexes created successfully") cursor.execute(''' CREATE TABLE IF NOT EXISTS AppSettings ( id INTEGER PRIMARY KEY, is_sso_enabled BOOLEAN DEFAULT FALSE ) ''') cursor.execute('SELECT id FROM AppSettings WHERE id = 1') if not cursor.fetchone(): cursor.execute('INSERT INTO AppSettings (id) VALUES (1)') run_v2_migrations(cursor, conn) conn.commit() conn.close() def run_v2_migrations(cursor, conn): """One-time schema cleanup for v2 upgrades from v1.x.""" logging.info("Running v2 database migrations...") cursor.execute('DROP TABLE IF EXISTS FeatureFlags') cursor.execute("SHOW COLUMNS FROM CustomFieldDefinition LIKE 'searchable'") if cursor.fetchone(): cursor.execute('ALTER TABLE CustomFieldDefinition DROP COLUMN searchable') logging.info("Dropped CustomFieldDefinition.searchable column") for perm_name in ('view_help',): cursor.execute('SELECT id FROM Permission WHERE name = %s', (perm_name,)) row = cursor.fetchone() if row: perm_id = row[0] cursor.execute('DELETE FROM RolePermission WHERE permission_id = %s', (perm_id,)) cursor.execute('DELETE FROM Permission WHERE id = %s', (perm_id,)) logging.info("Removed orphaned permission: %s", perm_name) cursor.execute("SHOW COLUMNS FROM Device LIKE 'device_type_id'") if cursor.fetchone(): cursor.execute(""" SELECT CONSTRAINT_NAME FROM information_schema.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'Device' AND COLUMN_NAME = 'device_type_id' AND REFERENCED_TABLE_NAME IS NOT NULL """) for (fk_name,) in cursor.fetchall(): cursor.execute(f'ALTER TABLE Device DROP FOREIGN KEY `{fk_name}`') cursor.execute('ALTER TABLE Device DROP COLUMN device_type_id') logging.info("Dropped Device.device_type_id column") cursor.execute("SHOW TABLES LIKE 'DeviceType'") if cursor.fetchone(): cursor.execute('DROP TABLE DeviceType') logging.info("Dropped DeviceType table") for perm_name in ( 'view_device_types', 'view_device_type_stats', 'view_devices_by_type', 'add_device_type', 'edit_device_type', 'delete_device_type', ): cursor.execute('SELECT id FROM Permission WHERE name = %s', (perm_name,)) row = cursor.fetchone() if row: perm_id = row[0] cursor.execute('DELETE FROM RolePermission WHERE permission_id = %s', (perm_id,)) cursor.execute('DELETE FROM Permission WHERE id = %s', (perm_id,)) logging.info("Removed orphaned permission: %s", perm_name) logging.info("v2 database migrations complete") def get_app_settings(conn): cursor = conn.cursor(dictionary=True) cursor.execute('SELECT is_sso_enabled FROM AppSettings WHERE id = 1') row = cursor.fetchone() if not row: return {'is_sso_enabled': False} return row def update_app_settings(conn, **fields): allowed = {'is_sso_enabled'} updates = {k: v for k, v in fields.items() if k in allowed} if not updates: return columns = ', '.join(f'{k} = %s' for k in updates) values = list(updates.values()) cursor = conn.cursor() cursor.execute(f'UPDATE AppSettings SET {columns} WHERE id = 1', values) conn.commit()