updated roles ant permissions
This commit is contained in:
BIN
instance/users.db
Normal file
BIN
instance/users.db
Normal file
Binary file not shown.
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 .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')
|
||||||
|
|||||||
@@ -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)})
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
<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;
|
||||||
|
|||||||
Reference in New Issue
Block a user