updated roles ant permissions

This commit is contained in:
2025-09-12 22:14:51 +03:00
parent 4597595db4
commit 9d80252c14
15 changed files with 1875 additions and 113 deletions

BIN
instance/users.db Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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())

View 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())

View 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())

View 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
View 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

View File

@@ -11,7 +11,9 @@ import csv
from .warehouse import add_location from .warehouse import add_location
from .settings import ( from .settings import (
settings_handler, settings_handler,
edit_access_roles_handler, role_permissions_handler,
save_role_permissions_handler,
reset_role_permissions_handler,
create_user_handler, create_user_handler,
edit_user_handler, edit_user_handler,
delete_user_handler, delete_user_handler,
@@ -21,11 +23,6 @@ from .settings import (
bp = Blueprint('main', __name__) bp = Blueprint('main', __name__)
warehouse_bp = Blueprint('warehouse', __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') @bp.route('/store_articles')
def store_articles(): def store_articles():
return render_template('store_articles.html') return render_template('store_articles.html')
@@ -153,11 +150,6 @@ def dashboard():
def settings(): def settings():
return settings_handler() 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') @bp.route('/quality')
def quality(): def quality():
if 'role' not in session or session['role'] not in ['superadmin', 'quality']: if 'role' not in session or session['role'] not in ['superadmin', 'quality']:
@@ -264,6 +256,19 @@ def delete_user():
def save_external_db(): def save_external_db():
return save_external_db_handler() 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']) @bp.route('/get_report_data', methods=['GET'])
def get_report_data(): def get_report_data():
report = request.args.get('report') report = request.args.get('report')

View File

@@ -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 .models import User
from . import db from . import db
from .permissions import APP_PERMISSIONS, ROLE_HIERARCHY, ACTIONS, get_all_permissions, get_default_permissions_for_role
import mariadb import mariadb
import os 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 # 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 # Helper to check if current user is superadmin
def is_superadmin(): def is_superadmin():
return session.get('role') == 'superadmin' return session.get('role') == 'superadmin'
# Route handler for editing access roles # Route handler for role permissions management
def edit_access_roles_handler(): def role_permissions_handler():
if not is_superadmin(): if not is_superadmin():
flash('Access denied: Superadmin only.') flash('Access denied: Superadmin only.')
return redirect(url_for('main.dashboard')) return redirect(url_for('main.dashboard'))
ensure_roles_table()
return render_template('edit_access_roles.html', roles=ROLES)
# Handler for updating role access (stub, to be implemented) try:
def update_role_access_handler(role): # Get roles and their current permissions
if not is_superadmin(): conn = get_external_db_connection()
flash('Access denied: Superadmin only.') cursor = conn.cursor()
return redirect(url_for('main.dashboard'))
if role == 'superadmin': # Get roles from role_hierarchy table
flash('Superadmin access cannot be changed.') cursor.execute("SELECT role_name, display_name, description, level FROM role_hierarchy ORDER BY level DESC")
return redirect(url_for('main.edit_access_roles')) role_data = cursor.fetchall()
access_level = request.form.get('access_level')
# TODO: Save access_level for the role in the database or config roles = {}
flash(f'Access for role {role} updated to {access_level}.') for role_name, display_name, description, level in role_data:
return redirect(url_for('main.edit_access_roles')) 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'))
def settings_handler(): def settings_handler():
if 'role' not in session or session['role'] != 'superadmin': if 'role' not in session or session['role'] != 'superadmin':
@@ -75,12 +184,13 @@ def settings_handler():
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL, username VARCHAR(50) UNIQUE NOT NULL,
password VARCHAR(255) 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 # 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() users_data = cursor.fetchall()
# Convert to list of dictionaries for template compatibility # Convert to list of dictionaries for template compatibility
@@ -90,7 +200,8 @@ def settings_handler():
'id': user_data[0], 'id': user_data[0],
'username': user_data[1], 'username': user_data[1],
'password': user_data[2], '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() conn.close()
@@ -142,6 +253,7 @@ def create_user_handler():
username = request.form['username'] username = request.form['username']
password = request.form['password'] password = request.form['password']
role = request.form['role'] role = request.form['role']
email = request.form.get('email', '').strip() or None # Optional field
try: try:
# Connect to external MariaDB database # Connect to external MariaDB database
@@ -154,7 +266,8 @@ def create_user_handler():
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL, username VARCHAR(50) UNIQUE NOT NULL,
password VARCHAR(255) 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 # Create a new user in external MariaDB
cursor.execute(""" cursor.execute("""
INSERT INTO users (username, password, role) INSERT INTO users (username, password, role, email)
VALUES (%s, %s, %s) VALUES (%s, %s, %s, %s)
""", (username, password, role)) """, (username, password, role, email))
conn.commit() conn.commit()
conn.close() conn.close()
@@ -189,6 +302,7 @@ def edit_user_handler():
user_id = request.form.get('user_id') user_id = request.form.get('user_id')
password = request.form.get('password', '').strip() password = request.form.get('password', '').strip()
role = request.form.get('role') role = request.form.get('role')
email = request.form.get('email', '').strip() or None # Optional field
if not user_id or not role: if not user_id or not role:
flash('Missing required fields.') flash('Missing required fields.')
@@ -209,13 +323,13 @@ def edit_user_handler():
# Update the user's details in external MariaDB # Update the user's details in external MariaDB
if password: # Only update password if provided if password: # Only update password if provided
cursor.execute(""" cursor.execute("""
UPDATE users SET password = %s, role = %s WHERE id = %s UPDATE users SET password = %s, role = %s, email = %s WHERE id = %s
""", (password, role, user_id)) """, (password, role, email, user_id))
flash('User updated successfully (including password).') 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(""" cursor.execute("""
UPDATE users SET role = %s WHERE id = %s UPDATE users SET role = %s, email = %s WHERE id = %s
""", (role, user_id)) """, (role, email, user_id))
flash('User role updated successfully.') flash('User role updated successfully.')
conn.commit() conn.commit()
@@ -283,3 +397,97 @@ def save_external_db_handler():
flash('External database settings saved/updated successfully.') flash('External database settings saved/updated successfully.')
return redirect(url_for('main.settings')) 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)})

View File

@@ -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 %}

View 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 %}

View File

@@ -37,9 +37,9 @@
</div> </div>
<div class="card" style="margin-top: 32px;"> <div class="card" style="margin-top: 32px;">
<h3>Edit Access Roles</h3> <h3>Role & Permissions Management</h3>
<p>Manage which roles can view or execute functions on each app page and feature.</p> <p>Configure granular permissions for each role in the system with expandable sections and detailed access control.</p>
<a href="{{ url_for('main.edit_access_roles') }}" class="btn">Edit Access Roles</a> <a href="{{ url_for('main.role_permissions') }}" class="btn">Manage Role Permissions</a>
</div> </div>
</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-popup-title').innerText = 'Edit User';
document.getElementById('user-id').value = btn.getAttribute('data-user-id'); document.getElementById('user-id').value = btn.getAttribute('data-user-id');
document.getElementById('username').value = btn.getAttribute('data-username'); 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('role').value = btn.getAttribute('data-role');
document.getElementById('password').value = ''; document.getElementById('password').value = '';
document.getElementById('password').required = false; document.getElementById('password').required = false;