feat: Implement warehouse module roles with auto-schema repair and remove module access section
- Add SchemaVerifier class for automatic database schema verification and repair - Implement warehouse_manager (Level 75) and warehouse_worker (Level 35) roles - Add zone-based access control for warehouse workers - Implement worker-manager binding system with zone filtering - Add comprehensive database auto-repair on Docker initialization - Remove Module Access section from user form (role-based access only) - Add autocomplete attributes to password fields for better UX - Include detailed documentation for warehouse implementation - Update initialize_db.py with schema verification as Step 0
This commit is contained in:
373
app/db_schema_verifier.py
Normal file
373
app/db_schema_verifier.py
Normal file
@@ -0,0 +1,373 @@
|
||||
"""
|
||||
Database Schema Verification & Repair Module
|
||||
Checks existing database structure and adds missing tables/columns/data
|
||||
Safely handles both fresh databases and existing installations
|
||||
"""
|
||||
import pymysql
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SchemaVerifier:
|
||||
"""Verify and repair database schema structure"""
|
||||
|
||||
def __init__(self, connection):
|
||||
self.conn = connection
|
||||
self.cursor = connection.cursor()
|
||||
self.changes_made = []
|
||||
|
||||
def verify_and_repair(self):
|
||||
"""
|
||||
Main verification and repair process
|
||||
|
||||
Returns:
|
||||
tuple: (success: bool, changes_summary: str)
|
||||
"""
|
||||
try:
|
||||
logger.info("=" * 60)
|
||||
logger.info("Starting database schema verification...")
|
||||
logger.info("=" * 60)
|
||||
|
||||
# 1. Verify tables
|
||||
self.verify_tables()
|
||||
|
||||
# 2. Verify columns in existing tables
|
||||
self.verify_columns()
|
||||
|
||||
# 3. Verify reference data (roles, etc)
|
||||
self.verify_reference_data()
|
||||
|
||||
# Commit all changes
|
||||
self.conn.commit()
|
||||
|
||||
# Generate summary
|
||||
summary = self.generate_summary()
|
||||
logger.info("=" * 60)
|
||||
logger.info("✓ Database schema verification complete")
|
||||
logger.info("=" * 60)
|
||||
|
||||
return True, summary
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"✗ Schema verification failed: {e}")
|
||||
self.conn.rollback()
|
||||
return False, f"Error: {str(e)}"
|
||||
|
||||
def table_exists(self, table_name):
|
||||
"""Check if table exists in database"""
|
||||
try:
|
||||
self.cursor.execute(f"""
|
||||
SELECT COUNT(*) FROM information_schema.TABLES
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s
|
||||
""", (table_name,))
|
||||
return self.cursor.fetchone()[0] > 0
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking if table {table_name} exists: {e}")
|
||||
return False
|
||||
|
||||
def column_exists(self, table_name, column_name):
|
||||
"""Check if column exists in table"""
|
||||
try:
|
||||
self.cursor.execute(f"""
|
||||
SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = %s
|
||||
AND COLUMN_NAME = %s
|
||||
""", (table_name, column_name))
|
||||
return self.cursor.fetchone()[0] > 0
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking column {table_name}.{column_name}: {e}")
|
||||
return False
|
||||
|
||||
def get_table_columns(self, table_name):
|
||||
"""Get all columns for a table"""
|
||||
try:
|
||||
self.cursor.execute(f"""
|
||||
SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_KEY, EXTRA
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s
|
||||
""", (table_name,))
|
||||
return {row[0]: row for row in self.cursor.fetchall()}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting columns for {table_name}: {e}")
|
||||
return {}
|
||||
|
||||
def verify_tables(self):
|
||||
"""Verify all required tables exist"""
|
||||
logger.info("Verifying tables...")
|
||||
|
||||
# Define all required tables with their schemas
|
||||
tables_to_verify = {
|
||||
'users': self.get_users_schema,
|
||||
'user_credentials': self.get_user_credentials_schema,
|
||||
'user_modules': self.get_user_modules_schema,
|
||||
'user_permissions': self.get_user_permissions_schema,
|
||||
'roles': self.get_roles_schema,
|
||||
'worker_manager_bindings': self.get_worker_manager_bindings_schema,
|
||||
'application_settings': self.get_application_settings_schema,
|
||||
'audit_logs': self.get_audit_logs_schema,
|
||||
'backup_schedules': self.get_backup_schedules_schema,
|
||||
}
|
||||
|
||||
for table_name, schema_getter in tables_to_verify.items():
|
||||
if not self.table_exists(table_name):
|
||||
logger.info(f" ⚠ Table '{table_name}' missing - creating...")
|
||||
try:
|
||||
sql = schema_getter()
|
||||
self.cursor.execute(sql)
|
||||
self.changes_made.append(f"Created table '{table_name}'")
|
||||
logger.info(f" ✓ Created table '{table_name}'")
|
||||
except Exception as e:
|
||||
logger.error(f" ✗ Failed to create table '{table_name}': {e}")
|
||||
raise
|
||||
else:
|
||||
logger.info(f" ✓ Table '{table_name}' exists")
|
||||
|
||||
def verify_columns(self):
|
||||
"""Verify all columns exist in tables"""
|
||||
logger.info("Verifying table columns...")
|
||||
|
||||
# Define required columns for each table
|
||||
columns_to_verify = {
|
||||
'users': [
|
||||
('id', 'INT', 'NO', 'PRI', 'auto_increment'),
|
||||
('username', 'VARCHAR(100)', 'NO', 'UNI', ''),
|
||||
('email', 'VARCHAR(255)', 'YES', 'UNI', ''),
|
||||
('full_name', 'VARCHAR(255)', 'YES', '', ''),
|
||||
('role', 'VARCHAR(100)', 'YES', 'MUL', ''),
|
||||
('is_active', 'TINYINT(1)', 'YES', '', ''),
|
||||
('created_at', 'TIMESTAMP', 'YES', '', 'DEFAULT_GENERATED'),
|
||||
('updated_at', 'TIMESTAMP', 'YES', '', 'DEFAULT_GENERATED'),
|
||||
],
|
||||
'worker_manager_bindings': [
|
||||
('id', 'INT', 'NO', 'PRI', 'auto_increment'),
|
||||
('manager_id', 'INT', 'NO', 'MUL', ''),
|
||||
('worker_id', 'INT', 'NO', '', ''),
|
||||
('warehouse_zone', 'VARCHAR(100)', 'YES', '', ''),
|
||||
('is_active', 'TINYINT(1)', 'YES', '', ''),
|
||||
('created_at', 'TIMESTAMP', 'YES', '', 'DEFAULT_GENERATED'),
|
||||
('updated_at', 'TIMESTAMP', 'YES', '', 'DEFAULT_GENERATED'),
|
||||
],
|
||||
'roles': [
|
||||
('id', 'INT', 'NO', 'PRI', 'auto_increment'),
|
||||
('name', 'VARCHAR(100)', 'NO', 'UNI', ''),
|
||||
('description', 'TEXT', 'YES', '', ''),
|
||||
('level', 'INT', 'YES', '', ''),
|
||||
('created_at', 'TIMESTAMP', 'YES', '', 'DEFAULT_GENERATED'),
|
||||
('updated_at', 'TIMESTAMP', 'YES', '', 'DEFAULT_GENERATED'),
|
||||
],
|
||||
}
|
||||
|
||||
for table_name, required_columns in columns_to_verify.items():
|
||||
if not self.table_exists(table_name):
|
||||
continue
|
||||
|
||||
logger.info(f" Checking columns in '{table_name}'...")
|
||||
existing_columns = self.get_table_columns(table_name)
|
||||
|
||||
for col_name, col_type, nullable, key, extra in required_columns:
|
||||
if col_name not in existing_columns:
|
||||
logger.info(f" ⚠ Column '{col_name}' missing - adding...")
|
||||
try:
|
||||
self.add_column(table_name, col_name, col_type, nullable)
|
||||
self.changes_made.append(f"Added column '{table_name}.{col_name}'")
|
||||
logger.info(f" ✓ Added column '{col_name}' to '{table_name}'")
|
||||
except Exception as e:
|
||||
logger.error(f" ✗ Failed to add column: {e}")
|
||||
raise
|
||||
else:
|
||||
logger.info(f" ✓ Column '{col_name}' exists")
|
||||
|
||||
def add_column(self, table_name, column_name, column_type, nullable='YES'):
|
||||
"""Add a missing column to a table"""
|
||||
null_clause = 'NULL' if nullable == 'YES' else 'NOT NULL'
|
||||
sql = f"ALTER TABLE {table_name} ADD COLUMN {column_name} {column_type} {null_clause}"
|
||||
self.cursor.execute(sql)
|
||||
|
||||
def verify_reference_data(self):
|
||||
"""Verify and add missing reference data (roles, etc)"""
|
||||
logger.info("Verifying reference data...")
|
||||
|
||||
if not self.table_exists('roles'):
|
||||
logger.info(" ⚠ Roles table doesn't exist, skipping reference data")
|
||||
return
|
||||
|
||||
# Define all required roles
|
||||
required_roles = [
|
||||
('superadmin', 'Super Administrator - Full system access', 100),
|
||||
('admin', 'Administrator - Administrative access', 90),
|
||||
('manager', 'Manager - Quality - Full access to assigned modules', 70),
|
||||
('warehouse_manager', 'Manager - Warehouse - Full warehouse module access', 75),
|
||||
('worker', 'Worker - Quality - Limited access', 50),
|
||||
('warehouse_worker', 'Worker - Warehouse - Input-only warehouse access', 35),
|
||||
]
|
||||
|
||||
logger.info(" Verifying roles...")
|
||||
for role_name, role_desc, role_level in required_roles:
|
||||
self.cursor.execute("SELECT id FROM roles WHERE name = %s", (role_name,))
|
||||
|
||||
if not self.cursor.fetchone():
|
||||
try:
|
||||
self.cursor.execute(
|
||||
"INSERT INTO roles (name, description, level) VALUES (%s, %s, %s)",
|
||||
(role_name, role_desc, role_level)
|
||||
)
|
||||
self.changes_made.append(f"Added role '{role_name}'")
|
||||
logger.info(f" ✓ Added role '{role_name}'")
|
||||
except Exception as e:
|
||||
logger.error(f" ✗ Failed to add role '{role_name}': {e}")
|
||||
else:
|
||||
logger.info(f" ✓ Role '{role_name}' exists")
|
||||
|
||||
def generate_summary(self):
|
||||
"""Generate a summary of changes made"""
|
||||
if not self.changes_made:
|
||||
summary = "✓ Database structure is correct - no changes needed"
|
||||
else:
|
||||
summary = f"✓ Database repair complete - {len(self.changes_made)} change(s) made:\n"
|
||||
for i, change in enumerate(self.changes_made, 1):
|
||||
summary += f" {i}. {change}\n"
|
||||
|
||||
return summary
|
||||
|
||||
# Schema definitions
|
||||
@staticmethod
|
||||
def get_users_schema():
|
||||
return """
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(100) UNIQUE NOT NULL,
|
||||
email VARCHAR(255) UNIQUE,
|
||||
full_name VARCHAR(255),
|
||||
role VARCHAR(100),
|
||||
is_active TINYINT(1) DEFAULT 1,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_role (role),
|
||||
INDEX idx_username (username),
|
||||
FOREIGN KEY (role) REFERENCES roles(name)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_user_credentials_schema():
|
||||
return """
|
||||
CREATE TABLE IF NOT EXISTS user_credentials (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL UNIQUE,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_roles_schema():
|
||||
return """
|
||||
CREATE TABLE IF NOT EXISTS roles (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(100) UNIQUE NOT NULL,
|
||||
description TEXT,
|
||||
level INT DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_user_modules_schema():
|
||||
return """
|
||||
CREATE TABLE IF NOT EXISTS user_modules (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
module_name VARCHAR(100) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY unique_user_module (user_id, module_name),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_user_permissions_schema():
|
||||
return """
|
||||
CREATE TABLE IF NOT EXISTS user_permissions (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
module_name VARCHAR(100) NOT NULL,
|
||||
section_name VARCHAR(100) NOT NULL,
|
||||
action_name VARCHAR(100) NOT NULL,
|
||||
granted TINYINT(1) DEFAULT 1,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY unique_permission (user_id, module_name, section_name, action_name),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_worker_manager_bindings_schema():
|
||||
return """
|
||||
CREATE TABLE IF NOT EXISTS worker_manager_bindings (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
manager_id INT NOT NULL,
|
||||
worker_id INT NOT NULL,
|
||||
warehouse_zone VARCHAR(100),
|
||||
is_active TINYINT(1) DEFAULT 1,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY unique_binding (manager_id, worker_id),
|
||||
FOREIGN KEY (manager_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (worker_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
CHECK (manager_id != worker_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_application_settings_schema():
|
||||
return """
|
||||
CREATE TABLE IF NOT EXISTS application_settings (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
setting_key VARCHAR(255) UNIQUE NOT NULL,
|
||||
setting_value LONGTEXT,
|
||||
setting_type VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_audit_logs_schema():
|
||||
return """
|
||||
CREATE TABLE IF NOT EXISTS audit_logs (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT,
|
||||
action VARCHAR(255) NOT NULL,
|
||||
entity_type VARCHAR(100),
|
||||
entity_id INT,
|
||||
details JSON,
|
||||
ip_address VARCHAR(45),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_user_id (user_id),
|
||||
INDEX idx_action (action),
|
||||
INDEX idx_created_at (created_at),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_backup_schedules_schema():
|
||||
return """
|
||||
CREATE TABLE IF NOT EXISTS backup_schedules (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
schedule_name VARCHAR(255) NOT NULL,
|
||||
frequency VARCHAR(50) NOT NULL,
|
||||
last_backup TIMESTAMP,
|
||||
next_backup TIMESTAMP,
|
||||
retention_days INT DEFAULT 30,
|
||||
is_active TINYINT(1) DEFAULT 1,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
"""
|
||||
Reference in New Issue
Block a user