updated roles ant permissions
This commit is contained in:
BIN
py_app/__pycache__/run.cpython-312.pyc
Normal file
BIN
py_app/__pycache__/run.cpython-312.pyc
Normal file
Binary file not shown.
BIN
py_app/app/__pycache__/permissions.cpython-312.pyc
Normal file
BIN
py_app/app/__pycache__/permissions.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
110
py_app/app/db_create_scripts/add_email_column.py
Normal file
110
py_app/app/db_create_scripts/add_email_column.py
Normal file
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import mariadb
|
||||
import os
|
||||
import sys
|
||||
|
||||
def get_external_db_connection():
|
||||
"""Reads the external_server.conf file and returns a MariaDB database connection."""
|
||||
# Get the instance folder path
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
instance_folder = os.path.join(current_dir, '../../instance')
|
||||
settings_file = os.path.join(instance_folder, 'external_server.conf')
|
||||
|
||||
if not os.path.exists(settings_file):
|
||||
raise FileNotFoundError(f"The external_server.conf file is missing: {settings_file}")
|
||||
|
||||
# Read settings from the configuration file
|
||||
settings = {}
|
||||
with open(settings_file, 'r') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line and '=' in line:
|
||||
key, value = line.split('=', 1)
|
||||
settings[key] = value
|
||||
|
||||
print(f"Connecting to MariaDB:")
|
||||
print(f" Host: {settings.get('server_domain', 'N/A')}")
|
||||
print(f" Port: {settings.get('port', 'N/A')}")
|
||||
print(f" Database: {settings.get('database_name', 'N/A')}")
|
||||
|
||||
return mariadb.connect(
|
||||
user=settings['username'],
|
||||
password=settings['password'],
|
||||
host=settings['server_domain'],
|
||||
port=int(settings['port']),
|
||||
database=settings['database_name']
|
||||
)
|
||||
|
||||
def main():
|
||||
try:
|
||||
print("=== Adding Email Column to Users Table ===")
|
||||
conn = get_external_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# First, check the current table structure
|
||||
print("\n1. Checking current table structure...")
|
||||
cursor.execute("DESCRIBE users")
|
||||
columns = cursor.fetchall()
|
||||
|
||||
has_email = False
|
||||
for column in columns:
|
||||
print(f" Column: {column[0]} ({column[1]})")
|
||||
if column[0] == 'email':
|
||||
has_email = True
|
||||
|
||||
if not has_email:
|
||||
print("\n2. Adding email column...")
|
||||
cursor.execute("ALTER TABLE users ADD COLUMN email VARCHAR(255)")
|
||||
conn.commit()
|
||||
print(" ✓ Email column added successfully")
|
||||
else:
|
||||
print("\n2. Email column already exists")
|
||||
|
||||
# Now check and display all users
|
||||
print("\n3. Current users in database:")
|
||||
cursor.execute("SELECT id, username, role, email FROM users")
|
||||
users = cursor.fetchall()
|
||||
|
||||
if users:
|
||||
print(f" Found {len(users)} users:")
|
||||
for user in users:
|
||||
email = user[3] if user[3] else "No email"
|
||||
print(f" - ID: {user[0]}, Username: {user[1]}, Role: {user[2]}, Email: {email}")
|
||||
else:
|
||||
print(" No users found - creating test users...")
|
||||
|
||||
# Create some test users
|
||||
test_users = [
|
||||
('admin_user', 'admin123', 'admin', 'admin@company.com'),
|
||||
('manager_user', 'manager123', 'manager', 'manager@company.com'),
|
||||
('warehouse_user', 'warehouse123', 'warehouse_manager', 'warehouse@company.com'),
|
||||
('quality_user', 'quality123', 'quality_manager', 'quality@company.com')
|
||||
]
|
||||
|
||||
for username, password, role, email in test_users:
|
||||
try:
|
||||
cursor.execute("""
|
||||
INSERT INTO users (username, password, role, email)
|
||||
VALUES (%s, %s, %s, %s)
|
||||
""", (username, password, role, email))
|
||||
print(f" ✓ Created user: {username} ({role})")
|
||||
except mariadb.IntegrityError as e:
|
||||
print(f" ⚠ User {username} already exists: {e}")
|
||||
|
||||
conn.commit()
|
||||
print(" ✓ Test users created successfully")
|
||||
|
||||
conn.close()
|
||||
print("\n=== Database Update Complete ===")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
105
py_app/app/db_create_scripts/check_external_db_users.py
Normal file
105
py_app/app/db_create_scripts/check_external_db_users.py
Normal file
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import mariadb
|
||||
import os
|
||||
import sys
|
||||
|
||||
def get_external_db_connection():
|
||||
"""Reads the external_server.conf file and returns a MariaDB database connection."""
|
||||
# Get the instance folder path
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
instance_folder = os.path.join(current_dir, '../../instance')
|
||||
settings_file = os.path.join(instance_folder, 'external_server.conf')
|
||||
|
||||
if not os.path.exists(settings_file):
|
||||
raise FileNotFoundError(f"The external_server.conf file is missing: {settings_file}")
|
||||
|
||||
# Read settings from the configuration file
|
||||
settings = {}
|
||||
with open(settings_file, 'r') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line and '=' in line:
|
||||
key, value = line.split('=', 1)
|
||||
settings[key] = value
|
||||
|
||||
print(f"Connecting to MariaDB with settings:")
|
||||
print(f" Host: {settings.get('server_domain', 'N/A')}")
|
||||
print(f" Port: {settings.get('port', 'N/A')}")
|
||||
print(f" Database: {settings.get('database_name', 'N/A')}")
|
||||
print(f" Username: {settings.get('username', 'N/A')}")
|
||||
|
||||
# Create a database connection
|
||||
return mariadb.connect(
|
||||
user=settings['username'],
|
||||
password=settings['password'],
|
||||
host=settings['server_domain'],
|
||||
port=int(settings['port']),
|
||||
database=settings['database_name']
|
||||
)
|
||||
|
||||
def main():
|
||||
try:
|
||||
print("=== Checking External MariaDB Database ===")
|
||||
conn = get_external_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Create users table if it doesn't exist
|
||||
print("\n1. Creating/verifying users table...")
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(50) UNIQUE NOT NULL,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
role VARCHAR(50) NOT NULL,
|
||||
email VARCHAR(255)
|
||||
)
|
||||
''')
|
||||
print(" ✓ Users table created/verified")
|
||||
|
||||
# Check existing users
|
||||
print("\n2. Checking existing users...")
|
||||
cursor.execute("SELECT id, username, role, email FROM users")
|
||||
users = cursor.fetchall()
|
||||
|
||||
if users:
|
||||
print(f" Found {len(users)} existing users:")
|
||||
for user in users:
|
||||
email = user[3] if user[3] else "No email"
|
||||
print(f" - ID: {user[0]}, Username: {user[1]}, Role: {user[2]}, Email: {email}")
|
||||
else:
|
||||
print(" No users found in external database")
|
||||
|
||||
# Create some test users
|
||||
print("\n3. Creating test users...")
|
||||
test_users = [
|
||||
('admin_user', 'admin123', 'admin', 'admin@company.com'),
|
||||
('manager_user', 'manager123', 'manager', 'manager@company.com'),
|
||||
('warehouse_user', 'warehouse123', 'warehouse_manager', 'warehouse@company.com'),
|
||||
('quality_user', 'quality123', 'quality_manager', 'quality@company.com')
|
||||
]
|
||||
|
||||
for username, password, role, email in test_users:
|
||||
try:
|
||||
cursor.execute("""
|
||||
INSERT INTO users (username, password, role, email)
|
||||
VALUES (%s, %s, %s, %s)
|
||||
""", (username, password, role, email))
|
||||
print(f" ✓ Created user: {username} ({role})")
|
||||
except mariadb.IntegrityError as e:
|
||||
print(f" ⚠ User {username} already exists: {e}")
|
||||
|
||||
conn.commit()
|
||||
print(" ✓ Test users created successfully")
|
||||
|
||||
conn.close()
|
||||
print("\n=== Database Check Complete ===")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
141
py_app/app/db_create_scripts/create_permissions_tables.py
Normal file
141
py_app/app/db_create_scripts/create_permissions_tables.py
Normal file
@@ -0,0 +1,141 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import mariadb
|
||||
import os
|
||||
import sys
|
||||
|
||||
def get_external_db_connection():
|
||||
"""Reads the external_server.conf file and returns a MariaDB database connection."""
|
||||
# Get the instance folder path
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
instance_folder = os.path.join(current_dir, '../../instance')
|
||||
settings_file = os.path.join(instance_folder, 'external_server.conf')
|
||||
|
||||
if not os.path.exists(settings_file):
|
||||
raise FileNotFoundError(f"The external_server.conf file is missing: {settings_file}")
|
||||
|
||||
# Read settings from the configuration file
|
||||
settings = {}
|
||||
with open(settings_file, 'r') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line and '=' in line:
|
||||
key, value = line.split('=', 1)
|
||||
settings[key] = value
|
||||
|
||||
return mariadb.connect(
|
||||
user=settings['username'],
|
||||
password=settings['password'],
|
||||
host=settings['server_domain'],
|
||||
port=int(settings['port']),
|
||||
database=settings['database_name']
|
||||
)
|
||||
|
||||
def main():
|
||||
try:
|
||||
print("=== Creating Permission Management Tables ===")
|
||||
conn = get_external_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 1. Create permissions table
|
||||
print("\n1. Creating permissions table...")
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS permissions (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
permission_key VARCHAR(255) UNIQUE NOT NULL,
|
||||
page VARCHAR(100) NOT NULL,
|
||||
page_name VARCHAR(255) NOT NULL,
|
||||
section VARCHAR(100) NOT NULL,
|
||||
section_name VARCHAR(255) NOT NULL,
|
||||
action VARCHAR(50) NOT NULL,
|
||||
action_name VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
)
|
||||
''')
|
||||
print(" ✓ Permissions table created/verified")
|
||||
|
||||
# 2. Create role_permissions table
|
||||
print("\n2. Creating role_permissions table...")
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS role_permissions (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
role VARCHAR(50) NOT NULL,
|
||||
permission_key VARCHAR(255) NOT NULL,
|
||||
granted BOOLEAN DEFAULT TRUE,
|
||||
granted_by VARCHAR(50),
|
||||
granted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY unique_role_permission (role, permission_key),
|
||||
FOREIGN KEY (permission_key) REFERENCES permissions(permission_key) ON DELETE CASCADE
|
||||
)
|
||||
''')
|
||||
print(" ✓ Role permissions table created/verified")
|
||||
|
||||
# 3. Create role_hierarchy table for role management
|
||||
print("\n3. Creating role_hierarchy table...")
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS role_hierarchy (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
role_name VARCHAR(50) UNIQUE NOT NULL,
|
||||
display_name VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
level INT DEFAULT 0,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
)
|
||||
''')
|
||||
print(" ✓ Role hierarchy table created/verified")
|
||||
|
||||
# 4. Create permission_audit_log table for tracking changes
|
||||
print("\n4. Creating permission_audit_log table...")
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS permission_audit_log (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
role VARCHAR(50) NOT NULL,
|
||||
permission_key VARCHAR(255) NOT NULL,
|
||||
action ENUM('granted', 'revoked') NOT NULL,
|
||||
changed_by VARCHAR(50) NOT NULL,
|
||||
changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
reason TEXT,
|
||||
ip_address VARCHAR(45)
|
||||
)
|
||||
''')
|
||||
print(" ✓ Permission audit log table created/verified")
|
||||
|
||||
conn.commit()
|
||||
|
||||
# 5. Check if we need to populate initial data
|
||||
print("\n5. Checking for existing data...")
|
||||
cursor.execute("SELECT COUNT(*) FROM permissions")
|
||||
permission_count = cursor.fetchone()[0]
|
||||
|
||||
if permission_count == 0:
|
||||
print(" No permissions found - will need to populate with default data")
|
||||
print(" Run 'populate_permissions.py' to initialize the permission system")
|
||||
else:
|
||||
print(f" Found {permission_count} existing permissions")
|
||||
|
||||
cursor.execute("SELECT COUNT(*) FROM role_hierarchy")
|
||||
role_count = cursor.fetchone()[0]
|
||||
|
||||
if role_count == 0:
|
||||
print(" No roles found - will need to populate with default roles")
|
||||
else:
|
||||
print(f" Found {role_count} existing roles")
|
||||
|
||||
conn.close()
|
||||
print("\n=== Permission Database Schema Created Successfully ===")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
143
py_app/app/db_create_scripts/populate_permissions.py
Normal file
143
py_app/app/db_create_scripts/populate_permissions.py
Normal file
@@ -0,0 +1,143 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import mariadb
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Add the app directory to the path so we can import our permissions module
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
|
||||
|
||||
from permissions import APP_PERMISSIONS, ROLE_HIERARCHY, ACTIONS, get_all_permissions, get_default_permissions_for_role
|
||||
|
||||
def get_external_db_connection():
|
||||
"""Reads the external_server.conf file and returns a MariaDB database connection."""
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
instance_folder = os.path.join(current_dir, '../../instance')
|
||||
settings_file = os.path.join(instance_folder, 'external_server.conf')
|
||||
|
||||
if not os.path.exists(settings_file):
|
||||
raise FileNotFoundError(f"The external_server.conf file is missing: {settings_file}")
|
||||
|
||||
settings = {}
|
||||
with open(settings_file, 'r') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line and '=' in line:
|
||||
key, value = line.split('=', 1)
|
||||
settings[key] = value
|
||||
|
||||
return mariadb.connect(
|
||||
user=settings['username'],
|
||||
password=settings['password'],
|
||||
host=settings['server_domain'],
|
||||
port=int(settings['port']),
|
||||
database=settings['database_name']
|
||||
)
|
||||
|
||||
def main():
|
||||
try:
|
||||
print("=== Populating Permission System ===")
|
||||
conn = get_external_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 1. Populate all permissions
|
||||
print("\n1. Populating permissions...")
|
||||
permissions = get_all_permissions()
|
||||
|
||||
for perm in permissions:
|
||||
try:
|
||||
cursor.execute('''
|
||||
INSERT INTO permissions (permission_key, page, page_name, section, section_name, action, action_name)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
page_name = VALUES(page_name),
|
||||
section_name = VALUES(section_name),
|
||||
action_name = VALUES(action_name),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
''', (
|
||||
perm['key'],
|
||||
perm['page'],
|
||||
perm['page_name'],
|
||||
perm['section'],
|
||||
perm['section_name'],
|
||||
perm['action'],
|
||||
perm['action_name']
|
||||
))
|
||||
except Exception as e:
|
||||
print(f" ⚠ Error inserting permission {perm['key']}: {e}")
|
||||
|
||||
conn.commit()
|
||||
print(f" ✓ Populated {len(permissions)} permissions")
|
||||
|
||||
# 2. Populate role hierarchy
|
||||
print("\n2. Populating role hierarchy...")
|
||||
for role_name, role_data in ROLE_HIERARCHY.items():
|
||||
try:
|
||||
cursor.execute('''
|
||||
INSERT INTO role_hierarchy (role_name, display_name, description, level)
|
||||
VALUES (%s, %s, %s, %s)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
display_name = VALUES(display_name),
|
||||
description = VALUES(description),
|
||||
level = VALUES(level),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
''', (
|
||||
role_name,
|
||||
role_data['name'],
|
||||
role_data['description'],
|
||||
role_data['level']
|
||||
))
|
||||
except Exception as e:
|
||||
print(f" ⚠ Error inserting role {role_name}: {e}")
|
||||
|
||||
conn.commit()
|
||||
print(f" ✓ Populated {len(ROLE_HIERARCHY)} roles")
|
||||
|
||||
# 3. Set default permissions for each role
|
||||
print("\n3. Setting default role permissions...")
|
||||
for role_name in ROLE_HIERARCHY.keys():
|
||||
default_permissions = get_default_permissions_for_role(role_name)
|
||||
|
||||
print(f" Setting permissions for {role_name}: {len(default_permissions)} permissions")
|
||||
|
||||
for permission_key in default_permissions:
|
||||
try:
|
||||
cursor.execute('''
|
||||
INSERT INTO role_permissions (role, permission_key, granted, granted_by)
|
||||
VALUES (%s, %s, TRUE, 'system')
|
||||
ON DUPLICATE KEY UPDATE
|
||||
granted = TRUE,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
''', (role_name, permission_key))
|
||||
except Exception as e:
|
||||
print(f" ⚠ Error setting permission {permission_key} for {role_name}: {e}")
|
||||
|
||||
conn.commit()
|
||||
|
||||
# 4. Show summary
|
||||
print("\n4. Permission Summary:")
|
||||
cursor.execute('''
|
||||
SELECT r.role_name, r.display_name, COUNT(rp.permission_key) as permission_count
|
||||
FROM role_hierarchy r
|
||||
LEFT JOIN role_permissions rp ON r.role_name = rp.role AND rp.granted = TRUE
|
||||
GROUP BY r.role_name, r.display_name
|
||||
ORDER BY r.level DESC
|
||||
''')
|
||||
|
||||
results = cursor.fetchall()
|
||||
for role_name, display_name, count in results:
|
||||
print(f" {display_name} ({role_name}): {count} permissions")
|
||||
|
||||
conn.close()
|
||||
print("\n=== Permission System Initialization Complete ===")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
344
py_app/app/permissions.py
Normal file
344
py_app/app/permissions.py
Normal file
@@ -0,0 +1,344 @@
|
||||
"""
|
||||
Role-Based Access Control (RBAC) System
|
||||
Hierarchical permission structure: Pages → Sections → Actions
|
||||
"""
|
||||
|
||||
# Permission Actions
|
||||
ACTIONS = {
|
||||
'view': 'View/Read Access',
|
||||
'create': 'Create/Add New',
|
||||
'edit': 'Edit/Modify',
|
||||
'delete': 'Delete/Remove',
|
||||
'upload': 'Upload Files',
|
||||
'download': 'Download Files',
|
||||
'export': 'Export Data',
|
||||
'import': 'Import Data'
|
||||
}
|
||||
|
||||
# Application Structure with Hierarchical Permissions
|
||||
APP_PERMISSIONS = {
|
||||
'dashboard': {
|
||||
'name': 'Dashboard',
|
||||
'sections': {
|
||||
'overview': {
|
||||
'name': 'Overview Statistics',
|
||||
'actions': ['view']
|
||||
},
|
||||
'recent_activity': {
|
||||
'name': 'Recent Activity Feed',
|
||||
'actions': ['view']
|
||||
},
|
||||
'quick_actions': {
|
||||
'name': 'Quick Action Buttons',
|
||||
'actions': ['view']
|
||||
}
|
||||
}
|
||||
},
|
||||
'settings': {
|
||||
'name': 'Settings & Administration',
|
||||
'sections': {
|
||||
'user_management': {
|
||||
'name': 'User Management',
|
||||
'actions': ['view', 'create', 'edit', 'delete']
|
||||
},
|
||||
'role_permissions': {
|
||||
'name': 'Role & Permissions',
|
||||
'actions': ['view', 'edit']
|
||||
},
|
||||
'external_database': {
|
||||
'name': 'External Database Config',
|
||||
'actions': ['view', 'edit']
|
||||
},
|
||||
'system_settings': {
|
||||
'name': 'System Configuration',
|
||||
'actions': ['view', 'edit']
|
||||
}
|
||||
}
|
||||
},
|
||||
'warehouse': {
|
||||
'name': 'Warehouse Management',
|
||||
'sections': {
|
||||
'inventory': {
|
||||
'name': 'Inventory Management',
|
||||
'actions': ['view', 'create', 'edit', 'delete', 'export']
|
||||
},
|
||||
'stock_movements': {
|
||||
'name': 'Stock Movements',
|
||||
'actions': ['view', 'create', 'edit', 'delete']
|
||||
},
|
||||
'receiving': {
|
||||
'name': 'Goods Receiving',
|
||||
'actions': ['view', 'create', 'edit', 'upload']
|
||||
},
|
||||
'shipping': {
|
||||
'name': 'Goods Shipping',
|
||||
'actions': ['view', 'create', 'edit', 'delete']
|
||||
},
|
||||
'locations': {
|
||||
'name': 'Storage Locations',
|
||||
'actions': ['view', 'create', 'edit', 'delete']
|
||||
},
|
||||
'reports': {
|
||||
'name': 'Warehouse Reports',
|
||||
'actions': ['view', 'export', 'download']
|
||||
}
|
||||
}
|
||||
},
|
||||
'quality': {
|
||||
'name': 'Quality Control',
|
||||
'sections': {
|
||||
'inspections': {
|
||||
'name': 'Quality Inspections',
|
||||
'actions': ['view', 'create', 'edit', 'delete']
|
||||
},
|
||||
'test_results': {
|
||||
'name': 'Test Results',
|
||||
'actions': ['view', 'create', 'edit', 'upload']
|
||||
},
|
||||
'certificates': {
|
||||
'name': 'Quality Certificates',
|
||||
'actions': ['view', 'create', 'edit', 'delete', 'upload', 'download']
|
||||
},
|
||||
'compliance': {
|
||||
'name': 'Compliance Management',
|
||||
'actions': ['view', 'create', 'edit']
|
||||
},
|
||||
'quality_reports': {
|
||||
'name': 'Quality Reports',
|
||||
'actions': ['view', 'export', 'download']
|
||||
}
|
||||
}
|
||||
},
|
||||
'production': {
|
||||
'name': 'Production Management',
|
||||
'sections': {
|
||||
'work_orders': {
|
||||
'name': 'Work Orders',
|
||||
'actions': ['view', 'create', 'edit', 'delete']
|
||||
},
|
||||
'production_lines': {
|
||||
'name': 'Production Lines',
|
||||
'actions': ['view', 'create', 'edit', 'delete']
|
||||
},
|
||||
'scheduling': {
|
||||
'name': 'Production Scheduling',
|
||||
'actions': ['view', 'create', 'edit', 'delete']
|
||||
},
|
||||
'equipment': {
|
||||
'name': 'Equipment Management',
|
||||
'actions': ['view', 'create', 'edit', 'delete']
|
||||
},
|
||||
'maintenance': {
|
||||
'name': 'Maintenance Records',
|
||||
'actions': ['view', 'create', 'edit', 'delete', 'upload']
|
||||
}
|
||||
}
|
||||
},
|
||||
'traceability': {
|
||||
'name': 'Product Traceability',
|
||||
'sections': {
|
||||
'batch_tracking': {
|
||||
'name': 'Batch Tracking',
|
||||
'actions': ['view', 'create', 'edit']
|
||||
},
|
||||
'lot_genealogy': {
|
||||
'name': 'Lot Genealogy',
|
||||
'actions': ['view', 'export']
|
||||
},
|
||||
'recall_management': {
|
||||
'name': 'Product Recall',
|
||||
'actions': ['view', 'create', 'edit', 'delete']
|
||||
},
|
||||
'chain_of_custody': {
|
||||
'name': 'Chain of Custody',
|
||||
'actions': ['view', 'create', 'edit']
|
||||
}
|
||||
}
|
||||
},
|
||||
'reports': {
|
||||
'name': 'Reports & Analytics',
|
||||
'sections': {
|
||||
'standard_reports': {
|
||||
'name': 'Standard Reports',
|
||||
'actions': ['view', 'export', 'download']
|
||||
},
|
||||
'custom_reports': {
|
||||
'name': 'Custom Reports',
|
||||
'actions': ['view', 'create', 'edit', 'delete', 'export']
|
||||
},
|
||||
'dashboards': {
|
||||
'name': 'Analytics Dashboards',
|
||||
'actions': ['view', 'create', 'edit', 'delete']
|
||||
},
|
||||
'data_export': {
|
||||
'name': 'Data Export Tools',
|
||||
'actions': ['view', 'export', 'download']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Role Hierarchy and Default Permissions
|
||||
ROLE_HIERARCHY = {
|
||||
'superadmin': {
|
||||
'name': 'Super Administrator',
|
||||
'description': 'Full system access - can manage all aspects including system configuration',
|
||||
'level': 100,
|
||||
'default_permissions': 'ALL' # Gets all permissions by default
|
||||
},
|
||||
'admin': {
|
||||
'name': 'Administrator',
|
||||
'description': 'Administrative access - can manage users and most system functions',
|
||||
'level': 90,
|
||||
'default_sections': [
|
||||
'dashboard',
|
||||
'settings.user_management',
|
||||
'settings.system_settings.view',
|
||||
'warehouse',
|
||||
'quality',
|
||||
'production',
|
||||
'reports',
|
||||
'traceability'
|
||||
],
|
||||
'restrictions': [
|
||||
'settings.role_permissions.edit', # Cannot modify role permissions
|
||||
'settings.external_database.edit', # Cannot modify external DB config
|
||||
]
|
||||
},
|
||||
'manager': {
|
||||
'name': 'Manager',
|
||||
'description': 'Management level access - can view and manage operational data',
|
||||
'level': 70,
|
||||
'default_sections': [
|
||||
'dashboard',
|
||||
'warehouse.inventory.view',
|
||||
'warehouse.reports.view',
|
||||
'quality.inspections.view',
|
||||
'quality.quality_reports.view',
|
||||
'production.work_orders',
|
||||
'reports.standard_reports'
|
||||
]
|
||||
},
|
||||
'warehouse_manager': {
|
||||
'name': 'Warehouse Manager',
|
||||
'description': 'Full warehouse access with limited system access',
|
||||
'level': 60,
|
||||
'default_sections': [
|
||||
'dashboard.overview.view',
|
||||
'warehouse', # Full warehouse access
|
||||
'traceability.batch_tracking',
|
||||
'reports.standard_reports.view'
|
||||
]
|
||||
},
|
||||
'warehouse_worker': {
|
||||
'name': 'Warehouse Worker',
|
||||
'description': 'Limited warehouse operations access',
|
||||
'level': 50,
|
||||
'default_sections': [
|
||||
'dashboard.overview.view',
|
||||
'warehouse.inventory.view',
|
||||
'warehouse.stock_movements',
|
||||
'warehouse.receiving',
|
||||
'warehouse.shipping.view'
|
||||
]
|
||||
},
|
||||
'quality_manager': {
|
||||
'name': 'Quality Manager',
|
||||
'description': 'Full quality control access',
|
||||
'level': 60,
|
||||
'default_sections': [
|
||||
'dashboard.overview.view',
|
||||
'quality', # Full quality access
|
||||
'traceability',
|
||||
'reports.standard_reports.view'
|
||||
]
|
||||
},
|
||||
'quality_worker': {
|
||||
'name': 'Quality Worker',
|
||||
'description': 'Limited quality control operations',
|
||||
'level': 50,
|
||||
'default_sections': [
|
||||
'dashboard.overview.view',
|
||||
'quality.inspections',
|
||||
'quality.test_results',
|
||||
'quality.certificates.view'
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
def get_permission_key(page, section, action):
|
||||
"""Generate a standardized permission key"""
|
||||
return f"{page}.{section}.{action}"
|
||||
|
||||
def parse_permission_key(permission_key):
|
||||
"""Parse a permission key into its components"""
|
||||
parts = permission_key.split('.')
|
||||
if len(parts) == 3:
|
||||
return parts[0], parts[1], parts[2]
|
||||
return None, None, None
|
||||
|
||||
def get_all_permissions():
|
||||
"""Get a flat list of all possible permissions"""
|
||||
permissions = []
|
||||
for page_key, page_data in APP_PERMISSIONS.items():
|
||||
for section_key, section_data in page_data['sections'].items():
|
||||
for action in section_data['actions']:
|
||||
permissions.append({
|
||||
'key': get_permission_key(page_key, section_key, action),
|
||||
'page': page_key,
|
||||
'page_name': page_data['name'],
|
||||
'section': section_key,
|
||||
'section_name': section_data['name'],
|
||||
'action': action,
|
||||
'action_name': ACTIONS.get(action, action)
|
||||
})
|
||||
return permissions
|
||||
|
||||
def get_default_permissions_for_role(role):
|
||||
"""Get default permissions for a specific role"""
|
||||
if role not in ROLE_HIERARCHY:
|
||||
return []
|
||||
|
||||
role_config = ROLE_HIERARCHY[role]
|
||||
|
||||
# Superadmin gets everything
|
||||
if role_config.get('default_permissions') == 'ALL':
|
||||
return [p['key'] for p in get_all_permissions()]
|
||||
|
||||
# Other roles get specific sections
|
||||
permissions = []
|
||||
default_sections = role_config.get('default_sections', [])
|
||||
|
||||
for section_pattern in default_sections:
|
||||
if section_pattern == 'dashboard':
|
||||
# Full dashboard access
|
||||
for section_key in APP_PERMISSIONS['dashboard']['sections'].keys():
|
||||
for action in APP_PERMISSIONS['dashboard']['sections'][section_key]['actions']:
|
||||
permissions.append(get_permission_key('dashboard', section_key, action))
|
||||
elif '.' in section_pattern:
|
||||
# Specific page.section or page.section.action
|
||||
parts = section_pattern.split('.')
|
||||
if len(parts) == 2: # page.section - all actions
|
||||
page, section = parts
|
||||
if page in APP_PERMISSIONS and section in APP_PERMISSIONS[page]['sections']:
|
||||
for action in APP_PERMISSIONS[page]['sections'][section]['actions']:
|
||||
permissions.append(get_permission_key(page, section, action))
|
||||
elif len(parts) == 3: # page.section.action - specific action
|
||||
page, section, action = parts
|
||||
if (page in APP_PERMISSIONS and
|
||||
section in APP_PERMISSIONS[page]['sections'] and
|
||||
action in APP_PERMISSIONS[page]['sections'][section]['actions']):
|
||||
permissions.append(get_permission_key(page, section, action))
|
||||
else:
|
||||
# Full page access
|
||||
page = section_pattern
|
||||
if page in APP_PERMISSIONS:
|
||||
for section_key, section_data in APP_PERMISSIONS[page]['sections'].items():
|
||||
for action in section_data['actions']:
|
||||
permissions.append(get_permission_key(page, section_key, action))
|
||||
|
||||
# Remove any restricted permissions
|
||||
restrictions = role_config.get('restrictions', [])
|
||||
permissions = [p for p in permissions if p not in restrictions]
|
||||
|
||||
return permissions
|
||||
@@ -11,7 +11,9 @@ import csv
|
||||
from .warehouse import add_location
|
||||
from .settings import (
|
||||
settings_handler,
|
||||
edit_access_roles_handler,
|
||||
role_permissions_handler,
|
||||
save_role_permissions_handler,
|
||||
reset_role_permissions_handler,
|
||||
create_user_handler,
|
||||
edit_user_handler,
|
||||
delete_user_handler,
|
||||
@@ -21,11 +23,6 @@ from .settings import (
|
||||
bp = Blueprint('main', __name__)
|
||||
warehouse_bp = Blueprint('warehouse', __name__)
|
||||
|
||||
@bp.route('/update_role_access/<role>', methods=['POST'])
|
||||
def update_role_access(role):
|
||||
from .settings import update_role_access_handler
|
||||
return update_role_access_handler(role)
|
||||
|
||||
@bp.route('/store_articles')
|
||||
def store_articles():
|
||||
return render_template('store_articles.html')
|
||||
@@ -153,11 +150,6 @@ def dashboard():
|
||||
def settings():
|
||||
return settings_handler()
|
||||
|
||||
# Route for editing access roles (superadmin only)
|
||||
@bp.route('/edit_access_roles')
|
||||
def edit_access_roles():
|
||||
return edit_access_roles_handler()
|
||||
|
||||
@bp.route('/quality')
|
||||
def quality():
|
||||
if 'role' not in session or session['role'] not in ['superadmin', 'quality']:
|
||||
@@ -264,6 +256,19 @@ def delete_user():
|
||||
def save_external_db():
|
||||
return save_external_db_handler()
|
||||
|
||||
# Role Permissions Management Routes
|
||||
@bp.route('/role_permissions')
|
||||
def role_permissions():
|
||||
return role_permissions_handler()
|
||||
|
||||
@bp.route('/settings/save_role_permissions', methods=['POST'])
|
||||
def save_role_permissions():
|
||||
return save_role_permissions_handler()
|
||||
|
||||
@bp.route('/settings/reset_role_permissions', methods=['POST'])
|
||||
def reset_role_permissions():
|
||||
return reset_role_permissions_handler()
|
||||
|
||||
@bp.route('/get_report_data', methods=['GET'])
|
||||
def get_report_data():
|
||||
report = request.args.get('report')
|
||||
|
||||
@@ -1,62 +1,171 @@
|
||||
from flask import render_template, request, session, redirect, url_for, flash, current_app
|
||||
from flask import render_template, request, session, redirect, url_for, flash, current_app, jsonify
|
||||
from .models import User
|
||||
from . import db
|
||||
from .permissions import APP_PERMISSIONS, ROLE_HIERARCHY, ACTIONS, get_all_permissions, get_default_permissions_for_role
|
||||
import mariadb
|
||||
import os
|
||||
import json
|
||||
|
||||
# Global permission cache to avoid repeated database queries
|
||||
_permission_cache = {}
|
||||
|
||||
def check_permission(permission_key, user_role=None):
|
||||
"""
|
||||
Check if the current user (or specified role) has a specific permission.
|
||||
|
||||
Args:
|
||||
permission_key (str): The permission key like 'settings.user_management.create'
|
||||
user_role (str, optional): Role to check. If None, uses current session role.
|
||||
|
||||
Returns:
|
||||
bool: True if user has the permission, False otherwise
|
||||
"""
|
||||
if user_role is None:
|
||||
user_role = session.get('role')
|
||||
|
||||
if not user_role:
|
||||
return False
|
||||
|
||||
# Superadmin always has all permissions
|
||||
if user_role == 'superadmin':
|
||||
return True
|
||||
|
||||
# Check cache first
|
||||
cache_key = f"{user_role}:{permission_key}"
|
||||
if cache_key in _permission_cache:
|
||||
return _permission_cache[cache_key]
|
||||
|
||||
try:
|
||||
conn = get_external_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
SELECT granted FROM role_permissions
|
||||
WHERE role = %s AND permission_key = %s
|
||||
""", (user_role, permission_key))
|
||||
|
||||
result = cursor.fetchone()
|
||||
conn.close()
|
||||
|
||||
# Cache the result
|
||||
has_permission = bool(result and result[0])
|
||||
_permission_cache[cache_key] = has_permission
|
||||
return has_permission
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error checking permission {permission_key} for role {user_role}: {e}")
|
||||
return False
|
||||
|
||||
def clear_permission_cache():
|
||||
"""Clear the permission cache (call after permission updates)"""
|
||||
global _permission_cache
|
||||
_permission_cache = {}
|
||||
|
||||
def require_permission(permission_key):
|
||||
"""
|
||||
Decorator to require a specific permission for a route.
|
||||
|
||||
Usage:
|
||||
@require_permission('settings.user_management.create')
|
||||
def create_user():
|
||||
...
|
||||
"""
|
||||
def decorator(f):
|
||||
from functools import wraps
|
||||
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if not check_permission(permission_key):
|
||||
flash(f'Access denied: You do not have permission to {permission_key}')
|
||||
return redirect(url_for('main.dashboard'))
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
return decorator
|
||||
|
||||
def get_user_permissions(user_role):
|
||||
"""Get all permissions for a specific role"""
|
||||
if user_role == 'superadmin':
|
||||
return [p['key'] for p in get_all_permissions()]
|
||||
|
||||
try:
|
||||
conn = get_external_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
SELECT permission_key FROM role_permissions
|
||||
WHERE role = %s AND granted = TRUE
|
||||
""", (user_role,))
|
||||
|
||||
result = cursor.fetchall()
|
||||
conn.close()
|
||||
|
||||
return [row[0] for row in result]
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error getting permissions for role {user_role}: {e}")
|
||||
return []
|
||||
|
||||
# Settings module logic
|
||||
import sqlite3
|
||||
import os
|
||||
def ensure_roles_table():
|
||||
instance_folder = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../instance'))
|
||||
if not os.path.exists(instance_folder):
|
||||
os.makedirs(instance_folder)
|
||||
db_path = os.path.join(instance_folder, 'users.db')
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS roles (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT UNIQUE NOT NULL,
|
||||
access_level TEXT NOT NULL,
|
||||
description TEXT
|
||||
)
|
||||
""")
|
||||
cursor.execute("""
|
||||
INSERT OR IGNORE INTO roles (name, access_level, description)
|
||||
VALUES (?, ?, ?)
|
||||
""", ('superadmin', 'full', 'Full access to all app areas and functions'))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
# List of roles (should match your app's roles)
|
||||
ROLES = [
|
||||
'superadmin', 'admin', 'manager', 'warehouse_manager', 'warehouse_worker', 'quality_manager', 'quality_worker'
|
||||
]
|
||||
|
||||
# Helper to check if current user is superadmin
|
||||
def is_superadmin():
|
||||
return session.get('role') == 'superadmin'
|
||||
|
||||
# Route handler for editing access roles
|
||||
def edit_access_roles_handler():
|
||||
# Route handler for role permissions management
|
||||
def role_permissions_handler():
|
||||
if not is_superadmin():
|
||||
flash('Access denied: Superadmin only.')
|
||||
return redirect(url_for('main.dashboard'))
|
||||
ensure_roles_table()
|
||||
return render_template('edit_access_roles.html', roles=ROLES)
|
||||
|
||||
try:
|
||||
# Get roles and their current permissions
|
||||
conn = get_external_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Get roles from role_hierarchy table
|
||||
cursor.execute("SELECT role_name, display_name, description, level FROM role_hierarchy ORDER BY level DESC")
|
||||
role_data = cursor.fetchall()
|
||||
|
||||
roles = {}
|
||||
for role_name, display_name, description, level in role_data:
|
||||
roles[role_name] = {
|
||||
'display_name': display_name,
|
||||
'description': description,
|
||||
'level': level
|
||||
}
|
||||
|
||||
# Get current role permissions
|
||||
cursor.execute("""
|
||||
SELECT role, permission_key
|
||||
FROM role_permissions
|
||||
WHERE granted = TRUE
|
||||
""")
|
||||
permission_data = cursor.fetchall()
|
||||
|
||||
role_permissions = {}
|
||||
for role, permission_key in permission_data:
|
||||
if role not in role_permissions:
|
||||
role_permissions[role] = []
|
||||
role_permissions[role].append(permission_key)
|
||||
|
||||
conn.close()
|
||||
|
||||
# Convert to JSON for JavaScript
|
||||
permissions_json = json.dumps(get_all_permissions())
|
||||
role_permissions_json = json.dumps(role_permissions)
|
||||
|
||||
return render_template('role_permissions.html',
|
||||
roles=roles,
|
||||
pages=APP_PERMISSIONS,
|
||||
action_names=ACTIONS,
|
||||
permissions_json=permissions_json,
|
||||
role_permissions_json=role_permissions_json)
|
||||
|
||||
except Exception as e:
|
||||
flash(f'Error loading role permissions: {e}')
|
||||
return redirect(url_for('main.settings'))
|
||||
|
||||
|
||||
# Handler for updating role access (stub, to be implemented)
|
||||
def update_role_access_handler(role):
|
||||
if not is_superadmin():
|
||||
flash('Access denied: Superadmin only.')
|
||||
return redirect(url_for('main.dashboard'))
|
||||
if role == 'superadmin':
|
||||
flash('Superadmin access cannot be changed.')
|
||||
return redirect(url_for('main.edit_access_roles'))
|
||||
access_level = request.form.get('access_level')
|
||||
# TODO: Save access_level for the role in the database or config
|
||||
flash(f'Access for role {role} updated to {access_level}.')
|
||||
return redirect(url_for('main.edit_access_roles'))
|
||||
|
||||
def settings_handler():
|
||||
if 'role' not in session or session['role'] != 'superadmin':
|
||||
@@ -75,12 +184,13 @@ def settings_handler():
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(50) UNIQUE NOT NULL,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
role VARCHAR(50) NOT NULL
|
||||
role VARCHAR(50) NOT NULL,
|
||||
email VARCHAR(255)
|
||||
)
|
||||
''')
|
||||
|
||||
# Get all users from external database
|
||||
cursor.execute("SELECT id, username, password, role FROM users")
|
||||
cursor.execute("SELECT id, username, password, role, email FROM users")
|
||||
users_data = cursor.fetchall()
|
||||
|
||||
# Convert to list of dictionaries for template compatibility
|
||||
@@ -90,7 +200,8 @@ def settings_handler():
|
||||
'id': user_data[0],
|
||||
'username': user_data[1],
|
||||
'password': user_data[2],
|
||||
'role': user_data[3]
|
||||
'role': user_data[3],
|
||||
'email': user_data[4] if len(user_data) > 4 else None
|
||||
})
|
||||
|
||||
conn.close()
|
||||
@@ -142,6 +253,7 @@ def create_user_handler():
|
||||
username = request.form['username']
|
||||
password = request.form['password']
|
||||
role = request.form['role']
|
||||
email = request.form.get('email', '').strip() or None # Optional field
|
||||
|
||||
try:
|
||||
# Connect to external MariaDB database
|
||||
@@ -154,7 +266,8 @@ def create_user_handler():
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(50) UNIQUE NOT NULL,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
role VARCHAR(50) NOT NULL
|
||||
role VARCHAR(50) NOT NULL,
|
||||
email VARCHAR(255)
|
||||
)
|
||||
''')
|
||||
|
||||
@@ -167,9 +280,9 @@ def create_user_handler():
|
||||
|
||||
# Create a new user in external MariaDB
|
||||
cursor.execute("""
|
||||
INSERT INTO users (username, password, role)
|
||||
VALUES (%s, %s, %s)
|
||||
""", (username, password, role))
|
||||
INSERT INTO users (username, password, role, email)
|
||||
VALUES (%s, %s, %s, %s)
|
||||
""", (username, password, role, email))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
@@ -189,6 +302,7 @@ def edit_user_handler():
|
||||
user_id = request.form.get('user_id')
|
||||
password = request.form.get('password', '').strip()
|
||||
role = request.form.get('role')
|
||||
email = request.form.get('email', '').strip() or None # Optional field
|
||||
|
||||
if not user_id or not role:
|
||||
flash('Missing required fields.')
|
||||
@@ -209,13 +323,13 @@ def edit_user_handler():
|
||||
# Update the user's details in external MariaDB
|
||||
if password: # Only update password if provided
|
||||
cursor.execute("""
|
||||
UPDATE users SET password = %s, role = %s WHERE id = %s
|
||||
""", (password, role, user_id))
|
||||
UPDATE users SET password = %s, role = %s, email = %s WHERE id = %s
|
||||
""", (password, role, email, user_id))
|
||||
flash('User updated successfully (including password).')
|
||||
else: # Just update role if no password provided
|
||||
else: # Just update role and email if no password provided
|
||||
cursor.execute("""
|
||||
UPDATE users SET role = %s WHERE id = %s
|
||||
""", (role, user_id))
|
||||
UPDATE users SET role = %s, email = %s WHERE id = %s
|
||||
""", (role, email, user_id))
|
||||
flash('User role updated successfully.')
|
||||
|
||||
conn.commit()
|
||||
@@ -283,3 +397,97 @@ def save_external_db_handler():
|
||||
|
||||
flash('External database settings saved/updated successfully.')
|
||||
return redirect(url_for('main.settings'))
|
||||
|
||||
def save_role_permissions_handler():
|
||||
"""Save role permissions via AJAX"""
|
||||
if not is_superadmin():
|
||||
return jsonify({'success': False, 'error': 'Access denied: Superadmin only.'})
|
||||
|
||||
try:
|
||||
data = request.get_json()
|
||||
role = data.get('role')
|
||||
permissions = data.get('permissions', [])
|
||||
|
||||
if not role:
|
||||
return jsonify({'success': False, 'error': 'Role is required'})
|
||||
|
||||
conn = get_external_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Clear existing permissions for this role
|
||||
cursor.execute("DELETE FROM role_permissions WHERE role = %s", (role,))
|
||||
|
||||
# Add new permissions
|
||||
current_user = session.get('username', 'system')
|
||||
for permission_key in permissions:
|
||||
cursor.execute("""
|
||||
INSERT INTO role_permissions (role, permission_key, granted, granted_by)
|
||||
VALUES (%s, %s, TRUE, %s)
|
||||
""", (role, permission_key, current_user))
|
||||
|
||||
# Log the change
|
||||
cursor.execute("""
|
||||
INSERT INTO permission_audit_log (role, permission_key, action, changed_by, reason)
|
||||
VALUES (%s, %s, 'bulk_update', %s, %s)
|
||||
""", (role, f"Updated {len(permissions)} permissions", current_user, f"Bulk update via UI"))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
# Clear permission cache since permissions changed
|
||||
clear_permission_cache()
|
||||
|
||||
return jsonify({'success': True, 'message': f'Saved {len(permissions)} permissions for {role}'})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)})
|
||||
|
||||
def reset_role_permissions_handler():
|
||||
"""Reset role permissions to defaults"""
|
||||
if not is_superadmin():
|
||||
return jsonify({'success': False, 'error': 'Access denied: Superadmin only.'})
|
||||
|
||||
try:
|
||||
data = request.get_json()
|
||||
role = data.get('role')
|
||||
|
||||
if not role:
|
||||
return jsonify({'success': False, 'error': 'Role is required'})
|
||||
|
||||
# Get default permissions for the role
|
||||
default_permissions = get_default_permissions_for_role(role)
|
||||
|
||||
conn = get_external_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Clear existing permissions for this role
|
||||
cursor.execute("DELETE FROM role_permissions WHERE role = %s", (role,))
|
||||
|
||||
# Add default permissions
|
||||
current_user = session.get('username', 'system')
|
||||
for permission_key in default_permissions:
|
||||
cursor.execute("""
|
||||
INSERT INTO role_permissions (role, permission_key, granted, granted_by)
|
||||
VALUES (%s, %s, TRUE, %s)
|
||||
""", (role, permission_key, current_user))
|
||||
|
||||
# Log the change
|
||||
cursor.execute("""
|
||||
INSERT INTO permission_audit_log (role, permission_key, action, changed_by, reason)
|
||||
VALUES (%s, %s, 'reset_defaults', %s, %s)
|
||||
""", (role, f"Reset {len(default_permissions)} permissions", current_user, "Reset to default permissions"))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
# Clear permission cache since permissions changed
|
||||
clear_permission_cache()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'permissions': default_permissions,
|
||||
'message': f'Reset {len(default_permissions)} permissions for {role} to defaults'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)})
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Edit Access Roles{% endblock %}
|
||||
{% block content %}
|
||||
<div class="card" style="max-width: 700px; margin: 32px auto;">
|
||||
<h3>Role Access Management</h3>
|
||||
<p>Configure which roles can view or execute functions on each app page and feature.</p>
|
||||
<table class="scan-table" style="width:100%;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Role</th>
|
||||
<th>Access Level</th>
|
||||
<th>Editable</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>superadmin</td>
|
||||
<td>Full access to all pages and functions</td>
|
||||
<td><span style="color:#888;">Not editable</span></td>
|
||||
</tr>
|
||||
{% for role in roles %}
|
||||
{% if role != 'superadmin' %}
|
||||
<tr>
|
||||
<td>{{ role }}</td>
|
||||
<td>
|
||||
<form method="POST" action="{{ url_for('main.update_role_access', role=role) }}">
|
||||
<select name="access_level">
|
||||
<option value="view">View Only</option>
|
||||
<option value="execute">View & Execute</option>
|
||||
<option value="none">No Access</option>
|
||||
</select>
|
||||
<button type="submit" class="btn">Save</button>
|
||||
</form>
|
||||
</td>
|
||||
<td>Editable</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<p style="margin-top:16px; color:#888;">Only superadmin users can view and manage role access.</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
748
py_app/app/templates/role_permissions.html
Normal file
748
py_app/app/templates/role_permissions.html
Normal file
@@ -0,0 +1,748 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Role Permissions Management{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<style>
|
||||
.permissions-container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
|
||||
.header-section {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
padding: 20px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 12px;
|
||||
color: white;
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.header-section h2 {
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.header-section p {
|
||||
margin: 0;
|
||||
opacity: 0.9;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.role-tabs {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 8px;
|
||||
margin-bottom: 25px;
|
||||
background-color: #f8f9fa;
|
||||
padding: 8px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.role-tab {
|
||||
padding: 15px 20px;
|
||||
cursor: pointer;
|
||||
background-color: white;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.role-tab.active {
|
||||
background: linear-gradient(135deg, #007bff, #0056b3);
|
||||
color: white;
|
||||
border-color: #0056b3;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0,123,255,0.3);
|
||||
}
|
||||
|
||||
.role-tab:hover:not(.active) {
|
||||
background-color: #e3f2fd;
|
||||
border-color: #007bff;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.permission-tree {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-section {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
|
||||
overflow: hidden;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.page-section:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 25px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.page-header {
|
||||
padding: 18px 24px;
|
||||
background: linear-gradient(135deg, #6c5ce7, #a29bfe);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.page-header::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
|
||||
transition: left 0.5s;
|
||||
}
|
||||
|
||||
.page-header:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.page-header.expanded {
|
||||
background: linear-gradient(135deg, #007bff, #0056b3);
|
||||
}
|
||||
|
||||
.expand-icon {
|
||||
transition: transform 0.3s;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.expanded .expand-icon {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.page-content {
|
||||
display: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.page-content.expanded {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.section-item {
|
||||
border-bottom: 1px solid #f1f1f1;
|
||||
}
|
||||
|
||||
.section-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
padding: 16px 24px;
|
||||
background: linear-gradient(135deg, #f8f9fa, #e9ecef);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-weight: 500;
|
||||
color: #495057;
|
||||
transition: all 0.3s ease;
|
||||
border-left: 3px solid transparent;
|
||||
}
|
||||
|
||||
.section-header:hover {
|
||||
background: linear-gradient(135deg, #e3f2fd, #bbdefb);
|
||||
border-left-color: #2196f3;
|
||||
color: #1565c0;
|
||||
}
|
||||
|
||||
.section-header.expanded {
|
||||
background: linear-gradient(135deg, #e3f2fd, #bbdefb);
|
||||
color: #1976d2;
|
||||
border-left-color: #1976d2;
|
||||
}
|
||||
|
||||
.section-content {
|
||||
display: none;
|
||||
padding: 20px 24px;
|
||||
background: linear-gradient(145deg, #ffffff, #f8f9fa);
|
||||
}
|
||||
|
||||
.section-content.expanded {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.action-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 12px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.action-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
border: 2px solid #f1f3f4;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.action-item:hover {
|
||||
border-color: #007bff;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0,123,255,0.15);
|
||||
}
|
||||
|
||||
.action-item.granted {
|
||||
border-color: #28a745;
|
||||
background: linear-gradient(145deg, #ffffff, #f8fff9);
|
||||
}
|
||||
|
||||
.action-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
color: #495057;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 12px;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.action-view { background-color: #28a745; }
|
||||
.action-create { background-color: #007bff; }
|
||||
.action-edit { background-color: #ffc107; color: #212529; }
|
||||
.action-delete { background-color: #dc3545; }
|
||||
.action-upload { background-color: #6f42c1; }
|
||||
.action-download { background-color: #20c997; }
|
||||
.action-export { background-color: #fd7e14; }
|
||||
.action-import { background-color: #e83e8c; }
|
||||
|
||||
.permission-toggle {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 50px;
|
||||
height: 25px;
|
||||
}
|
||||
|
||||
.permission-toggle input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.toggle-slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #ccc;
|
||||
transition: 0.4s;
|
||||
border-radius: 25px;
|
||||
}
|
||||
|
||||
.toggle-slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 19px;
|
||||
width: 19px;
|
||||
left: 3px;
|
||||
bottom: 3px;
|
||||
background-color: white;
|
||||
transition: 0.4s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
input:checked + .toggle-slider {
|
||||
background-color: #007bff;
|
||||
}
|
||||
|
||||
input:checked + .toggle-slider:before {
|
||||
transform: translateX(25px);
|
||||
}
|
||||
|
||||
.permission-summary {
|
||||
background: linear-gradient(145deg, #ffffff, #f8f9fa);
|
||||
padding: 25px;
|
||||
margin-bottom: 25px;
|
||||
border-radius: 12px;
|
||||
border-left: 5px solid #007bff;
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.summary-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||
gap: 20px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 15px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.stat-item:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: #007bff;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 11px;
|
||||
color: #6c757d;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
margin-top: 30px;
|
||||
padding: 25px;
|
||||
background: linear-gradient(145deg, #ffffff, #f8f9fa);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #007bff, #0056b3);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: linear-gradient(135deg, #0056b3, #004494);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: linear-gradient(135deg, #6c757d, #545b62);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: linear-gradient(135deg, #545b62, #383d41);
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: linear-gradient(135deg, #28a745, #1e7e34);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background: linear-gradient(135deg, #1e7e34, #155724);
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.permissions-container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.role-tabs {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.permission-tree {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.action-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.summary-stats {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="permissions-container">
|
||||
<div class="header-section">
|
||||
<h2>Role Permissions Management</h2>
|
||||
<p>Configure granular access permissions for each role in the system</p>
|
||||
</div>
|
||||
|
||||
<!-- Role Tabs -->
|
||||
<div class="role-tabs">
|
||||
{% for role_name, role_data in roles.items() %}
|
||||
<div class="role-tab {% if loop.first %}active{% endif %}"
|
||||
data-role="{{ role_name }}"
|
||||
onclick="switchRole('{{ role_name }}')">
|
||||
<div>{{ role_data.display_name }}</div>
|
||||
<small>Level {{ role_data.level }}</small>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% for role_name, role_data in roles.items() %}
|
||||
<div class="role-content" id="role-{{ role_name }}"
|
||||
style="{% if not loop.first %}display: none;{% endif %}">
|
||||
|
||||
<!-- Permission Summary -->
|
||||
<div class="permission-summary">
|
||||
<h4>{{ role_data.display_name }} Permissions Summary</h4>
|
||||
<p>{{ role_data.description }}</p>
|
||||
<div class="summary-stats">
|
||||
<div class="stat-item">
|
||||
<div class="stat-number" id="total-permissions-{{ role_name }}">0</div>
|
||||
<div class="stat-label">Total Permissions</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-number" id="granted-permissions-{{ role_name }}">0</div>
|
||||
<div class="stat-label">Granted</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-number" id="denied-permissions-{{ role_name }}">0</div>
|
||||
<div class="stat-label">Denied</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Permission Tree -->
|
||||
<div class="permission-tree">
|
||||
{% for page_key, page_data in pages.items() %}
|
||||
<div class="page-section">
|
||||
<div class="page-header" onclick="togglePage('{{ role_name }}', '{{ page_key }}')">
|
||||
<span>
|
||||
<span class="expand-icon">▶</span>
|
||||
{{ page_data.name }}
|
||||
</span>
|
||||
<span class="page-stats" id="page-stats-{{ role_name }}-{{ page_key }}">0/0</span>
|
||||
</div>
|
||||
<div class="page-content" id="page-content-{{ role_name }}-{{ page_key }}">
|
||||
{% for section_key, section_data in page_data.sections.items() %}
|
||||
<div class="section-item">
|
||||
<div class="section-header" onclick="toggleSection('{{ role_name }}', '{{ page_key }}', '{{ section_key }}')">
|
||||
<span>
|
||||
<span class="expand-icon">▶</span>
|
||||
{{ section_data.name }}
|
||||
</span>
|
||||
<span class="section-stats" id="section-stats-{{ role_name }}-{{ page_key }}-{{ section_key }}">0/{{ section_data.actions|length }}</span>
|
||||
</div>
|
||||
<div class="section-content" id="section-content-{{ role_name }}-{{ page_key }}-{{ section_key }}">
|
||||
<div class="action-grid">
|
||||
{% for action in section_data.actions %}
|
||||
<div class="action-item" id="action-{{ role_name }}-{{ page_key }}-{{ section_key }}-{{ action }}">
|
||||
<div class="action-label">
|
||||
<div class="action-icon action-{{ action }}">
|
||||
{% if action == 'view' %}👁{% elif action == 'create' %}➕{% elif action == 'edit' %}✏️{% elif action == 'delete' %}🗑{% elif action == 'upload' %}📤{% elif action == 'download' %}📥{% elif action == 'export' %}📊{% elif action == 'import' %}📈{% endif %}
|
||||
</div>
|
||||
<span>{{ action_names.get(action, action) }}</span>
|
||||
</div>
|
||||
<label class="permission-toggle">
|
||||
<input type="checkbox"
|
||||
data-role="{{ role_name }}"
|
||||
data-permission="{{ page_key }}.{{ section_key }}.{{ action }}"
|
||||
onchange="updatePermission('{{ role_name }}', '{{ page_key }}.{{ section_key }}.{{ action }}', this.checked)">
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="action-buttons">
|
||||
<button class="btn btn-secondary" onclick="resetToDefaults('{{ role_name }}')">Reset to Defaults</button>
|
||||
<button class="btn btn-primary" onclick="savePermissions('{{ role_name }}')">Save Changes</button>
|
||||
<button class="btn btn-success" onclick="copyFromRole('{{ role_name }}')">Copy from Another Role</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentRole = '{{ roles.keys()|first }}';
|
||||
let permissions = {{ permissions_json|safe }};
|
||||
let rolePermissions = {{ role_permissions_json|safe }};
|
||||
|
||||
function switchRole(roleName) {
|
||||
// Hide all role contents
|
||||
document.querySelectorAll('.role-content').forEach(content => {
|
||||
content.style.display = 'none';
|
||||
});
|
||||
|
||||
// Remove active class from all tabs
|
||||
document.querySelectorAll('.role-tab').forEach(tab => {
|
||||
tab.classList.remove('active');
|
||||
});
|
||||
|
||||
// Show selected role content
|
||||
document.getElementById('role-' + roleName).style.display = 'block';
|
||||
|
||||
// Add active class to selected tab
|
||||
document.querySelector(`[data-role="${roleName}"]`).classList.add('active');
|
||||
|
||||
currentRole = roleName;
|
||||
loadPermissions(roleName);
|
||||
}
|
||||
|
||||
function togglePage(roleName, pageKey) {
|
||||
const header = document.querySelector(`#page-content-${roleName}-${pageKey}`).previousElementSibling;
|
||||
const content = document.getElementById(`page-content-${roleName}-${pageKey}`);
|
||||
|
||||
if (content.classList.contains('expanded')) {
|
||||
content.classList.remove('expanded');
|
||||
header.classList.remove('expanded');
|
||||
} else {
|
||||
content.classList.add('expanded');
|
||||
header.classList.add('expanded');
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSection(roleName, pageKey, sectionKey) {
|
||||
const header = document.querySelector(`#section-content-${roleName}-${pageKey}-${sectionKey}`).previousElementSibling;
|
||||
const content = document.getElementById(`section-content-${roleName}-${pageKey}-${sectionKey}`);
|
||||
|
||||
if (content.classList.contains('expanded')) {
|
||||
content.classList.remove('expanded');
|
||||
header.classList.remove('expanded');
|
||||
} else {
|
||||
content.classList.add('expanded');
|
||||
header.classList.add('expanded');
|
||||
}
|
||||
}
|
||||
|
||||
function loadPermissions(roleName) {
|
||||
const rolePerms = rolePermissions[roleName] || [];
|
||||
|
||||
// Reset all checkboxes and update visual feedback
|
||||
document.querySelectorAll(`input[data-role="${roleName}"]`).forEach(checkbox => {
|
||||
const permissionKey = checkbox.getAttribute('data-permission');
|
||||
const isGranted = rolePerms.includes(permissionKey);
|
||||
checkbox.checked = isGranted;
|
||||
|
||||
// Update visual feedback
|
||||
const parts = permissionKey.split('.');
|
||||
const actionElement = document.getElementById(`action-${roleName}-${parts[0]}-${parts[1]}-${parts[2]}`);
|
||||
if (actionElement) {
|
||||
if (isGranted) {
|
||||
actionElement.classList.add('granted');
|
||||
} else {
|
||||
actionElement.classList.remove('granted');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
updateAllStats(roleName);
|
||||
}
|
||||
|
||||
function updatePermission(roleName, permissionKey, granted) {
|
||||
// Update local state
|
||||
if (!rolePermissions[roleName]) {
|
||||
rolePermissions[roleName] = [];
|
||||
}
|
||||
|
||||
if (granted && !rolePermissions[roleName].includes(permissionKey)) {
|
||||
rolePermissions[roleName].push(permissionKey);
|
||||
} else if (!granted) {
|
||||
const index = rolePermissions[roleName].indexOf(permissionKey);
|
||||
if (index > -1) {
|
||||
rolePermissions[roleName].splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Update visual feedback
|
||||
const parts = permissionKey.split('.');
|
||||
const actionElement = document.getElementById(`action-${roleName}-${parts[0]}-${parts[1]}-${parts[2]}`);
|
||||
if (actionElement) {
|
||||
if (granted) {
|
||||
actionElement.classList.add('granted');
|
||||
} else {
|
||||
actionElement.classList.remove('granted');
|
||||
}
|
||||
}
|
||||
|
||||
updateAllStats(roleName);
|
||||
}
|
||||
|
||||
function updateAllStats(roleName) {
|
||||
const totalPerms = document.querySelectorAll(`input[data-role="${roleName}"]`).length;
|
||||
const grantedPerms = document.querySelectorAll(`input[data-role="${roleName}"]:checked`).length;
|
||||
|
||||
document.getElementById(`total-permissions-${roleName}`).textContent = totalPerms;
|
||||
document.getElementById(`granted-permissions-${roleName}`).textContent = grantedPerms;
|
||||
document.getElementById(`denied-permissions-${roleName}`).textContent = totalPerms - grantedPerms;
|
||||
|
||||
// Update page and section stats
|
||||
updatePageStats(roleName);
|
||||
}
|
||||
|
||||
function updatePageStats(roleName) {
|
||||
const pages = {{ pages.keys()|list|tojson }};
|
||||
|
||||
pages.forEach(pageKey => {
|
||||
const pageCheckboxes = document.querySelectorAll(`input[data-role="${roleName}"][data-permission^="${pageKey}."]`);
|
||||
const pageGranted = Array.from(pageCheckboxes).filter(cb => cb.checked).length;
|
||||
const pageTotal = pageCheckboxes.length;
|
||||
|
||||
const pageStatsEl = document.getElementById(`page-stats-${roleName}-${pageKey}`);
|
||||
if (pageStatsEl) {
|
||||
pageStatsEl.textContent = `${pageGranted}/${pageTotal}`;
|
||||
}
|
||||
|
||||
// Update section stats within this page
|
||||
const sections = {{ pages|tojson }};
|
||||
Object.keys(sections[pageKey].sections).forEach(sectionKey => {
|
||||
const sectionCheckboxes = document.querySelectorAll(`input[data-role="${roleName}"][data-permission^="${pageKey}.${sectionKey}."]`);
|
||||
const sectionGranted = Array.from(sectionCheckboxes).filter(cb => cb.checked).length;
|
||||
const sectionTotal = sectionCheckboxes.length;
|
||||
|
||||
const sectionStatsEl = document.getElementById(`section-stats-${roleName}-${pageKey}-${sectionKey}`);
|
||||
if (sectionStatsEl) {
|
||||
sectionStatsEl.textContent = `${sectionGranted}/${sectionTotal}`;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function savePermissions(roleName) {
|
||||
const permissions = rolePermissions[roleName] || [];
|
||||
|
||||
fetch('/settings/save_role_permissions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
role: roleName,
|
||||
permissions: permissions
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert('Permissions saved successfully!');
|
||||
} else {
|
||||
alert('Error saving permissions: ' + data.error);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
alert('Error saving permissions: ' + error);
|
||||
});
|
||||
}
|
||||
|
||||
function resetToDefaults(roleName) {
|
||||
if (confirm('Are you sure you want to reset permissions to defaults? This will overwrite all current settings for this role.')) {
|
||||
fetch('/settings/reset_role_permissions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
role: roleName
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
rolePermissions[roleName] = data.permissions;
|
||||
loadPermissions(roleName);
|
||||
alert('Permissions reset to defaults!');
|
||||
} else {
|
||||
alert('Error resetting permissions: ' + data.error);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
alert('Error resetting permissions: ' + error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function copyFromRole(targetRole) {
|
||||
const roles = Object.keys(rolePermissions);
|
||||
const sourceRole = prompt(`Enter the role to copy permissions from:\nAvailable roles: ${roles.join(', ')}`);
|
||||
|
||||
if (sourceRole && roles.includes(sourceRole) && sourceRole !== targetRole) {
|
||||
if (confirm(`Copy all permissions from ${sourceRole} to ${targetRole}?`)) {
|
||||
rolePermissions[targetRole] = [...(rolePermissions[sourceRole] || [])];
|
||||
loadPermissions(targetRole);
|
||||
alert('Permissions copied successfully!');
|
||||
}
|
||||
} else if (sourceRole) {
|
||||
alert('Invalid role name or same as target role.');
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadPermissions(currentRole);
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -37,9 +37,9 @@
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-top: 32px;">
|
||||
<h3>Edit Access Roles</h3>
|
||||
<p>Manage which roles can view or execute functions on each app page and feature.</p>
|
||||
<a href="{{ url_for('main.edit_access_roles') }}" class="btn">Edit Access Roles</a>
|
||||
<h3>Role & Permissions Management</h3>
|
||||
<p>Configure granular permissions for each role in the system with expandable sections and detailed access control.</p>
|
||||
<a href="{{ url_for('main.role_permissions') }}" class="btn">Manage Role Permissions</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -105,6 +105,7 @@ Array.from(document.getElementsByClassName('edit-user-btn')).forEach(function(bt
|
||||
document.getElementById('user-popup-title').innerText = 'Edit User';
|
||||
document.getElementById('user-id').value = btn.getAttribute('data-user-id');
|
||||
document.getElementById('username').value = btn.getAttribute('data-username');
|
||||
document.getElementById('email').value = btn.getAttribute('data-email') || '';
|
||||
document.getElementById('role').value = btn.getAttribute('data-role');
|
||||
document.getElementById('password').value = '';
|
||||
document.getElementById('password').required = false;
|
||||
|
||||
Reference in New Issue
Block a user