feat: role based access control

This commit is contained in:
2025-11-06 13:08:10 +00:00
parent 73a94943cf
commit 3bf2697010
7 changed files with 1305 additions and 213 deletions
+7 -3
View File
@@ -1,5 +1,5 @@
from flask import Flask
from db import init_db, hash_password
from flask import Flask, session
from db import init_db, hash_password, get_db_connection
from routes import register_routes
import os
from dotenv import load_dotenv
@@ -26,10 +26,14 @@ def inject_env_vars():
except Exception:
pass
# Import has_permission from routes after routes are registered
from routes import has_permission
return {
'NAME': os.environ.get('NAME', 'JDB-NET'),
'LOGO_PNG': os.environ.get('LOGO_PNG', 'https://assets.s3.jdbnet.co.uk/logo/128x128.png'),
'VERSION': version
'VERSION': version,
'has_permission': has_permission
}
register_routes(app)
+186 -2
View File
@@ -149,9 +149,193 @@ def init_db(app=None):
except mysql.connector.Error as e:
if e.errno != 1061 and e.errno != 1826 and 'Duplicate' not in str(e):
raise
# Create Role table
cursor.execute('''
CREATE TABLE IF NOT EXISTS Role (
id INTEGER PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(255) NOT NULL UNIQUE,
description TEXT
)
''')
# Create Permission table
cursor.execute('''
CREATE TABLE IF NOT EXISTS Permission (
id INTEGER PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(255) NOT NULL UNIQUE,
description TEXT,
category VARCHAR(255)
)
''')
# Create RolePermission junction table
cursor.execute('''
CREATE TABLE IF NOT EXISTS RolePermission (
role_id INTEGER NOT NULL,
permission_id INTEGER NOT NULL,
PRIMARY KEY (role_id, permission_id),
FOREIGN KEY (role_id) REFERENCES Role(id) ON DELETE CASCADE,
FOREIGN KEY (permission_id) REFERENCES Permission(id) ON DELETE CASCADE
)
''')
# Add role_id column to User table if it doesn't exist
cursor.execute("SHOW COLUMNS FROM User LIKE 'role_id'")
if not cursor.fetchone():
cursor.execute('ALTER TABLE User ADD COLUMN role_id INTEGER DEFAULT NULL')
try:
cursor.execute('ALTER TABLE User ADD CONSTRAINT fk_user_role FOREIGN KEY (role_id) REFERENCES Role(id)')
except mysql.connector.Error as e:
if e.errno != 1061 and e.errno != 1826 and 'Duplicate' not in str(e):
raise
# Define all permissions with categories
permissions = [
# View permissions
('view_index', 'View Home/Index page', 'View'),
('view_devices', 'View Devices page', 'View'),
('view_device', 'View Device details', 'View'),
('view_subnet', 'View Subnet details', 'View'),
('view_racks', 'View Racks page', 'View'),
('view_rack', 'View Rack details', 'View'),
('view_audit', 'View Audit Log', 'View'),
('view_admin', 'View Admin panel', 'View'),
('view_users', 'View Users page', 'View'),
('view_device_types', 'View Device Types page', 'View'),
('view_device_type_stats', 'View Device Type Statistics', 'View'),
('view_devices_by_type', 'View Devices by Type', 'View'),
('view_dhcp', 'View DHCP configuration', 'View'),
('view_help', 'View Help page', 'View'),
# Device permissions
('add_device', 'Add new device', 'Device'),
('edit_device', 'Edit device (rename, description, type)', 'Device'),
('delete_device', 'Delete device', 'Device'),
('add_device_ip', 'Add IP address to device', 'Device'),
('remove_device_ip', 'Remove IP address from device', 'Device'),
# Subnet permissions
('add_subnet', 'Add new subnet', 'Subnet'),
('edit_subnet', 'Edit subnet (name, CIDR, site)', 'Subnet'),
('delete_subnet', 'Delete subnet', 'Subnet'),
('export_subnet_csv', 'Export subnet as CSV', 'Subnet'),
# Rack permissions
('add_rack', 'Add new rack', 'Rack'),
('delete_rack', 'Delete rack', 'Rack'),
('add_device_to_rack', 'Add device to rack', 'Rack'),
('remove_device_from_rack', 'Remove device from rack', 'Rack'),
('add_nonnet_device_to_rack', 'Add non-networked device to rack', 'Rack'),
('export_rack_csv', 'Export rack as CSV', 'Rack'),
# DHCP permissions
('configure_dhcp', 'Configure DHCP pools', 'DHCP'),
# Device Type permissions
('add_device_type', 'Add device type', 'Device Type'),
('edit_device_type', 'Edit device type', 'Device Type'),
('delete_device_type', 'Delete device type', 'Device Type'),
# Admin permissions
('manage_users', 'Manage users (add, edit, delete)', 'Admin'),
('manage_roles', 'Manage roles and permissions', 'Admin'),
]
# Insert permissions
for perm_name, perm_desc, perm_category in permissions:
cursor.execute('SELECT id FROM Permission WHERE name = %s', (perm_name,))
if not cursor.fetchone():
cursor.execute('INSERT INTO Permission (name, description, category) VALUES (%s, %s, %s)',
(perm_name, perm_desc, perm_category))
# Create default roles if they don't exist
cursor.execute('SELECT id FROM Role WHERE name = %s', ('admin',))
admin_role = cursor.fetchone()
if not admin_role:
cursor.execute('INSERT INTO Role (name, description) VALUES (%s, %s)',
('admin', 'Administrator with full access to all features'))
admin_role_id = cursor.lastrowid
else:
admin_role_id = admin_role[0]
cursor.execute('SELECT id FROM Role WHERE name = %s', ('user',))
user_role = cursor.fetchone()
if not user_role:
cursor.execute('INSERT INTO Role (name, description) VALUES (%s, %s)',
('user', 'Standard user with access to most features except admin functions'))
user_role_id = cursor.lastrowid
else:
user_role_id = user_role[0]
cursor.execute('SELECT id FROM Role WHERE name = %s', ('view_only',))
view_only_role = cursor.fetchone()
if not view_only_role:
cursor.execute('INSERT INTO Role (name, description) VALUES (%s, %s)',
('view_only', 'View-only user with read-only access to all pages'))
view_only_role_id = cursor.lastrowid
else:
view_only_role_id = view_only_role[0]
# Assign all permissions to admin role
cursor.execute('SELECT id FROM Permission')
all_permission_ids = [row[0] for row in cursor.fetchall()]
for perm_id in all_permission_ids:
cursor.execute('SELECT role_id FROM RolePermission WHERE role_id = %s AND permission_id = %s',
(admin_role_id, perm_id))
if not cursor.fetchone():
cursor.execute('INSERT INTO RolePermission (role_id, permission_id) VALUES (%s, %s)',
(admin_role_id, perm_id))
# Assign non-admin permissions to user role
non_admin_permissions = [
'view_index', 'view_devices', 'view_device', 'view_subnet', 'view_racks', 'view_rack',
'view_audit', 'view_device_types', 'view_device_type_stats', 'view_devices_by_type',
'view_dhcp', 'view_help',
'add_device', 'edit_device', 'delete_device', 'add_device_ip', 'remove_device_ip',
'add_subnet', 'edit_subnet', 'delete_subnet', 'export_subnet_csv',
'add_rack', 'delete_rack', 'add_device_to_rack', 'remove_device_from_rack',
'add_nonnet_device_to_rack', 'export_rack_csv',
'configure_dhcp',
'add_device_type', 'edit_device_type', 'delete_device_type'
]
for perm_name in non_admin_permissions:
cursor.execute('SELECT id FROM Permission WHERE name = %s', (perm_name,))
perm_result = cursor.fetchone()
if perm_result:
perm_id = perm_result[0]
cursor.execute('SELECT role_id FROM RolePermission WHERE role_id = %s AND permission_id = %s',
(user_role_id, perm_id))
if not cursor.fetchone():
cursor.execute('INSERT INTO RolePermission (role_id, permission_id) VALUES (%s, %s)',
(user_role_id, perm_id))
# Assign view-only permissions to view_only role
# Same view permissions as user role, but excluding admin views (view_admin, view_users)
view_only_permissions = [
'view_index', 'view_devices', 'view_device', 'view_subnet', 'view_racks', 'view_rack',
'view_audit', 'view_device_types', 'view_device_type_stats', 'view_devices_by_type',
'view_dhcp', 'view_help'
]
for perm_name in view_only_permissions:
cursor.execute('SELECT id FROM Permission WHERE name = %s', (perm_name,))
perm_result = cursor.fetchone()
if perm_result:
perm_id = perm_result[0]
cursor.execute('SELECT role_id FROM RolePermission WHERE role_id = %s AND permission_id = %s',
(view_only_role_id, perm_id))
if not cursor.fetchone():
cursor.execute('INSERT INTO RolePermission (role_id, permission_id) VALUES (%s, %s)',
(view_only_role_id, perm_id))
# Assign existing users to 'admin' role if they don't have a role
# This ensures existing users maintain admin access
cursor.execute('UPDATE User SET role_id = %s WHERE role_id IS NULL', (admin_role_id,))
cursor.execute('SELECT COUNT(*) FROM User')
if cursor.fetchone()[0] == 0:
cursor.execute('''INSERT INTO User (name, email, password) VALUES (%s, %s, %s)''',
('admin', 'admin@example.com', hash_password('password')))
cursor.execute('''INSERT INTO User (name, email, password, role_id) VALUES (%s, %s, %s, %s)''',
('admin', 'admin@example.com', hash_password('password'), admin_role_id))
conn.commit()
conn.close()
+340 -138
View File
@@ -1,4 +1,4 @@
from flask import render_template, request, redirect, url_for, send_from_directory, send_file, session
from flask import render_template, request, redirect, url_for, send_from_directory, send_file, session, abort
from db import init_db, hash_password, get_db_connection, verify_password
from ipaddress import ip_network
from functools import wraps
@@ -18,6 +18,53 @@ def login_required(f):
return f(*args, **kwargs)
return decorated_function
def has_permission(permission_name, user_id=None, conn=None):
"""Check if a user has a specific permission"""
if user_id is None:
user_id = session.get('user_id')
if not user_id:
return False
close_conn = False
if conn is None:
from flask import current_app
conn = get_db_connection(current_app)
close_conn = True
try:
cursor = conn.cursor()
# Get user's role
cursor.execute('SELECT role_id FROM User WHERE id = %s', (user_id,))
role_result = cursor.fetchone()
if not role_result or not role_result[0]:
return False
role_id = role_result[0]
# Check if role has the permission
cursor.execute('''
SELECT COUNT(*) FROM RolePermission rp
JOIN Permission p ON rp.permission_id = p.id
WHERE rp.role_id = %s AND p.name = %s
''', (role_id, permission_name))
result = cursor.fetchone()
return result[0] > 0 if result else False
finally:
if close_conn:
conn.close()
def permission_required(permission_name):
"""Decorator to require a specific permission"""
def decorator(f):
@wraps(f)
@login_required
def decorated_function(*args, **kwargs):
if not has_permission(permission_name):
abort(403)
return f(*args, **kwargs)
return decorated_function
return decorator
def add_audit_log(user_id, action, details=None, subnet_id=None, conn=None):
import datetime
close_conn = False
@@ -65,7 +112,7 @@ def register_routes(app):
return redirect(url_for('login'))
@app.route('/')
@login_required
@permission_required('view_index')
def index():
from flask import current_app
with get_db_connection(current_app) as conn:
@@ -81,7 +128,7 @@ def register_routes(app):
return render_with_user('index.html', sites_subnets=sites_subnets)
@app.route('/devices')
@login_required
@permission_required('view_devices')
def devices():
from flask import current_app
with get_db_connection(current_app) as conn:
@@ -105,7 +152,7 @@ def register_routes(app):
return render_with_user('devices.html', sites_devices=sites_devices, device_ips=device_ips)
@app.route('/add_device', methods=['GET', 'POST'])
@login_required
@permission_required('add_device')
def add_device():
from flask import current_app
with get_db_connection(current_app) as conn:
@@ -125,7 +172,7 @@ def register_routes(app):
return render_with_user('add_device.html', device_types=device_types)
@app.route('/device/<int:device_id>')
@login_required
@permission_required('view_device')
def device(device_id):
from flask import current_app
with get_db_connection(current_app) as conn:
@@ -162,7 +209,7 @@ def register_routes(app):
return render_with_user('device.html', device={'id': device[0], 'name': device[1], 'description': device[2], 'device_type_id': device[3]}, subnets=subnets, device_ips=device_ips, available_ips_by_subnet=available_ips_by_subnet, device_types=device_types)
@app.route('/update_device_type', methods=['POST'])
@login_required
@permission_required('edit_device')
def update_device_type():
device_id = request.form['device_id']
device_type_id = request.form['device_type_id']
@@ -176,7 +223,7 @@ def register_routes(app):
return redirect(url_for('device', device_id=device_id))
@app.route('/device/<int:device_id>/add_ip', methods=['POST'])
@login_required
@permission_required('add_device_ip')
def device_add_ip(device_id):
subnet_id = request.form['subnet_id']
ip_id = request.form['ip_id']
@@ -234,7 +281,7 @@ def register_routes(app):
return redirect(url_for('device', device_id=device_id))
@app.route('/device/<int:device_id>/delete_ip', methods=['POST'])
@login_required
@permission_required('remove_device_ip')
def device_delete_ip(device_id):
device_ip_id = request.form['device_ip_id']
user_name = get_current_user_name()
@@ -260,7 +307,7 @@ def register_routes(app):
return redirect(url_for('device', device_id=device_id))
@app.route('/delete_device', methods=['POST'])
@login_required
@permission_required('delete_device')
def delete_device():
device_id = request.form['device_id']
user_name = get_current_user_name()
@@ -283,7 +330,7 @@ def register_routes(app):
return redirect(url_for('devices'))
@app.route('/subnet/<int:subnet_id>')
@login_required
@permission_required('view_subnet')
def subnet(subnet_id):
from flask import current_app
with get_db_connection(current_app) as conn:
@@ -308,7 +355,7 @@ def register_routes(app):
return render_with_user('subnet.html', subnet={'id': subnet[0], 'name': subnet[1], 'cidr': subnet[2]}, ip_addresses=ip_addresses_with_device)
@app.route('/add_subnet', methods=['POST'])
@login_required
@permission_required('add_subnet')
def add_subnet():
name = request.form['name']
cidr = request.form['cidr']
@@ -332,8 +379,29 @@ def register_routes(app):
logging.info(f"User {user_name} added subnet '{name}' ({cidr}) at site '{site}'.")
return redirect(url_for('admin'))
@app.route('/edit_subnet', methods=['POST'])
@permission_required('edit_subnet')
def edit_subnet():
subnet_id = request.form['subnet_id']
name = request.form['name']
cidr = request.form['cidr']
site = request.form['site']
user_name = get_current_user_name()
from flask import current_app
with get_db_connection(current_app) as conn:
cursor = conn.cursor()
cursor.execute('SELECT name, cidr FROM Subnet WHERE id = %s', (subnet_id,))
old_subnet = cursor.fetchone()
if old_subnet:
old_name, old_cidr = old_subnet
cursor.execute('UPDATE Subnet SET name = %s, cidr = %s, site = %s WHERE id = %s', (name, cidr, site, subnet_id))
add_audit_log(session['user_id'], 'edit_subnet', f"Edited subnet from {old_name} ({old_cidr}) to {name} ({cidr}) at site {site}", subnet_id, conn=conn)
conn.commit()
logging.info(f"User {user_name} edited subnet {subnet_id}.")
return redirect(url_for('admin'))
@app.route('/delete_subnet', methods=['POST'])
@login_required
@permission_required('delete_subnet')
def delete_subnet():
subnet_id = request.form['subnet_id']
user_name = get_current_user_name()
@@ -355,54 +423,175 @@ def register_routes(app):
return redirect(url_for('admin'))
@app.route('/admin', methods=['GET', 'POST'])
@login_required
@permission_required('view_admin')
def admin():
from flask import current_app
with get_db_connection(current_app) as conn:
cursor = conn.cursor()
cursor.execute('SELECT id, name, cidr FROM Subnet')
subnets = [dict(id=row[0], name=row[1], cidr=row[2]) for row in cursor.fetchall()]
return render_with_user('admin.html', subnets=subnets)
cursor.execute('SELECT id, name, cidr, site FROM Subnet ORDER BY site, name')
subnets = [dict(id=row[0], name=row[1], cidr=row[2], site=row[3] or 'Unassigned') for row in cursor.fetchall()]
return render_with_user('admin.html', subnets=subnets,
can_add_subnet=has_permission('add_subnet'),
can_edit_subnet=has_permission('edit_subnet'),
can_delete_subnet=has_permission('delete_subnet'))
@app.route('/users', methods=['GET', 'POST'])
@login_required
@permission_required('view_users')
def users():
from flask import current_app
with get_db_connection(current_app) as conn:
cursor = conn.cursor()
error = None
if request.method == 'POST':
action = request.form['action']
user_name = get_current_user_name()
if action == 'add':
name = request.form['name']
email = request.form['email']
password = hash_password(request.form['password'])
cursor.execute('INSERT INTO User (name, email, password) VALUES (%s, %s, %s)', (name, email, password))
logging.info(f"User {user_name} added user '{name}' ({email}).")
elif action == 'edit':
user_id = request.form['user_id']
name = request.form['name']
email = request.form['email']
password = request.form['password']
if password:
password = hash_password(password)
cursor.execute('UPDATE User SET name=%s, email=%s, password=%s WHERE id=%s', (name, email, password, user_id))
# User management actions
if action == 'add_user':
if not has_permission('manage_users', conn=conn):
error = 'You do not have permission to add users.'
else:
cursor.execute('UPDATE User SET name=%s, email=%s WHERE id=%s', (name, email, user_id))
logging.info(f"User {user_name} edited user {user_id}.")
elif action == 'delete':
user_id = request.form['user_id']
cursor.execute('UPDATE User SET name=%s WHERE id=%s', ('Deleted User', user_id))
cursor.execute('UPDATE AuditLog SET user_id=NULL WHERE user_id=%s', (user_id,))
cursor.execute('DELETE FROM User WHERE id=%s', (user_id,))
logging.info(f"User {user_name} deleted user {user_id}.")
conn.commit()
cursor.execute('SELECT id, name, email FROM User')
name = request.form['name']
email = request.form['email']
password = hash_password(request.form['password'])
role_id = request.form.get('role_id')
if role_id:
cursor.execute('INSERT INTO User (name, email, password, role_id) VALUES (%s, %s, %s, %s)', (name, email, password, role_id))
else:
cursor.execute('INSERT INTO User (name, email, password) VALUES (%s, %s, %s)', (name, email, password))
logging.info(f"User {user_name} added user '{name}' ({email}).")
conn.commit()
elif action == 'edit_user':
if not has_permission('manage_users', conn=conn):
error = 'You do not have permission to edit users.'
else:
user_id = request.form['user_id']
name = request.form['name']
email = request.form['email']
password = request.form.get('password', '')
role_id = request.form.get('role_id')
if password:
password = hash_password(password)
if role_id:
cursor.execute('UPDATE User SET name=%s, email=%s, password=%s, role_id=%s WHERE id=%s', (name, email, password, role_id, user_id))
else:
cursor.execute('UPDATE User SET name=%s, email=%s, password=%s WHERE id=%s', (name, email, password, user_id))
else:
if role_id:
cursor.execute('UPDATE User SET name=%s, email=%s, role_id=%s WHERE id=%s', (name, email, role_id, user_id))
else:
cursor.execute('UPDATE User SET name=%s, email=%s WHERE id=%s', (name, email, user_id))
logging.info(f"User {user_name} edited user {user_id}.")
conn.commit()
elif action == 'delete_user':
if not has_permission('manage_users', conn=conn):
error = 'You do not have permission to delete users.'
else:
user_id = request.form['user_id']
cursor.execute('UPDATE User SET name=%s WHERE id=%s', ('Deleted User', user_id))
cursor.execute('UPDATE AuditLog SET user_id=NULL WHERE user_id=%s', (user_id,))
cursor.execute('DELETE FROM User WHERE id=%s', (user_id,))
logging.info(f"User {user_name} deleted user {user_id}.")
conn.commit()
# Role management actions
elif action == 'add_role':
if not has_permission('manage_roles', conn=conn):
error = 'You do not have permission to add roles.'
else:
role_name = request.form['role_name'].strip()
role_description = request.form.get('role_description', '').strip()
if not role_name:
error = 'Role name is required.'
else:
try:
cursor.execute('INSERT INTO Role (name, description) VALUES (%s, %s)', (role_name, role_description))
role_id = cursor.lastrowid
# Get selected permissions
permission_ids = request.form.getlist('permissions')
for perm_id in permission_ids:
cursor.execute('INSERT INTO RolePermission (role_id, permission_id) VALUES (%s, %s)', (role_id, perm_id))
conn.commit()
logging.info(f"User {user_name} added role '{role_name}'.")
except mysql.connector.IntegrityError as e:
if e.errno == 1062: # Duplicate entry
error = f"Role '{role_name}' already exists."
else:
raise
elif action == 'edit_role':
if not has_permission('manage_roles', conn=conn):
error = 'You do not have permission to edit roles.'
else:
role_id = request.form['role_id']
role_name = request.form['role_name'].strip()
role_description = request.form.get('role_description', '').strip()
if not role_name:
error = 'Role name is required.'
else:
try:
cursor.execute('UPDATE Role SET name=%s, description=%s WHERE id=%s', (role_name, role_description, role_id))
# Update permissions
cursor.execute('DELETE FROM RolePermission WHERE role_id=%s', (role_id,))
permission_ids = request.form.getlist('permissions')
for perm_id in permission_ids:
cursor.execute('INSERT INTO RolePermission (role_id, permission_id) VALUES (%s, %s)', (role_id, perm_id))
conn.commit()
logging.info(f"User {user_name} edited role {role_id}.")
except mysql.connector.IntegrityError as e:
if e.errno == 1062: # Duplicate entry
error = f"Role '{role_name}' already exists."
else:
raise
elif action == 'delete_role':
if not has_permission('manage_roles', conn=conn):
error = 'You do not have permission to delete roles.'
else:
role_id = request.form['role_id']
# Check if any users are using this role
cursor.execute('SELECT COUNT(*) FROM User WHERE role_id = %s', (role_id,))
user_count = cursor.fetchone()[0]
if user_count > 0:
cursor.execute('SELECT name FROM Role WHERE id = %s', (role_id,))
role_name = cursor.fetchone()[0]
error = f"Cannot delete role '{role_name}' because {user_count} user(s) are using it."
else:
cursor.execute('SELECT name FROM Role WHERE id = %s', (role_id,))
role_name = cursor.fetchone()[0]
cursor.execute('DELETE FROM Role WHERE id = %s', (role_id,))
conn.commit()
logging.info(f"User {user_name} deleted role '{role_name}'.")
# Get users with their roles
cursor.execute('''
SELECT u.id, u.name, u.email, r.id as role_id, r.name as role_name
FROM User u
LEFT JOIN Role r ON u.role_id = r.id
ORDER BY u.name
''')
users = cursor.fetchall()
return render_with_user('users.html', users=users)
# Get all roles
cursor.execute('SELECT id, name, description FROM Role ORDER BY name')
roles = cursor.fetchall()
# Get all permissions grouped by category
cursor.execute('SELECT id, name, description, category FROM Permission ORDER BY category, name')
permissions = cursor.fetchall()
# Get permissions for each role
role_permissions = {}
for role in roles:
role_id = role[0]
cursor.execute('''
SELECT permission_id FROM RolePermission WHERE role_id = %s
''', (role_id,))
role_permissions[role_id] = [row[0] for row in cursor.fetchall()]
return render_with_user('users.html', users=users, roles=roles, permissions=permissions, role_permissions=role_permissions, error=error,
can_manage_users=has_permission('manage_users'), can_manage_roles=has_permission('manage_roles'))
@app.route('/audit')
@login_required
@permission_required('view_audit')
def audit():
PER_PAGE = 25
page = int(request.args.get('page', 1))
@@ -446,7 +635,7 @@ def register_routes(app):
return render_with_user('audit.html', logs=logs, users=users, subnets=subnets, actions=actions, devices=devices, page=page, total_pages=total_pages, query_args=query_args)
@app.route('/get_available_ips')
@login_required
@permission_required('view_device')
def get_available_ips():
subnet_id = request.args.get('subnet_id')
from flask import current_app
@@ -457,7 +646,7 @@ def register_routes(app):
return {'available_ips': available_ips}
@app.route('/rename_device', methods=['POST'])
@login_required
@permission_required('edit_device')
def rename_device():
device_id = request.form['device_id']
new_name = request.form['new_name']
@@ -475,7 +664,7 @@ def register_routes(app):
return redirect(url_for('device', device_id=device_id))
@app.route('/update_device_description', methods=['POST'])
@login_required
@permission_required('edit_device')
def update_device_description():
device_id = request.form['device_id']
description = request.form['description']
@@ -489,7 +678,7 @@ def register_routes(app):
return redirect(url_for('device', device_id=device_id))
@app.route('/subnet/<int:subnet_id>/export_csv')
@login_required
@permission_required('export_subnet_csv')
def export_subnet_csv(subnet_id):
from flask import current_app
with get_db_connection(current_app) as conn:
@@ -533,7 +722,7 @@ def register_routes(app):
)
@app.route('/subnet/<int:subnet_id>/dhcp', methods=['GET', 'POST'])
@login_required
@permission_required('view_dhcp')
def dhcp_pool(subnet_id):
error = None
from flask import current_app
@@ -547,47 +736,50 @@ def register_routes(app):
if row:
dhcp_pool = {'start_ip': row[0], 'end_ip': row[1], 'excluded_ips': row[2] if len(row) > 2 else ''}
if request.method == 'POST':
user_name = get_current_user_name()
if 'remove' in request.form:
cursor.execute('DELETE FROM DHCPPool WHERE subnet_id = %s', (subnet_id,))
cursor.execute('UPDATE IPAddress SET hostname=NULL WHERE subnet_id=%s AND hostname="DHCP"', (subnet_id,))
conn.commit()
dhcp_pool = None
add_audit_log(session['user_id'], 'dhcp_pool_remove', f"Removed DHCP pool for subnet {subnet[1]} ({subnet[2]})", subnet_id, conn=conn)
if not has_permission('configure_dhcp', conn=conn):
error = 'You do not have permission to configure DHCP pools.'
else:
start_ip = request.form['start_ip']
end_ip = request.form['end_ip']
excluded_ips = request.form.get('excluded_ips', '').replace(' ', '')
excluded_list = [ip for ip in excluded_ips.split(',') if ip]
cursor.execute('SELECT ip FROM IPAddress WHERE subnet_id = %s', (subnet_id,))
all_ips = [row[0] for row in cursor.fetchall()]
if start_ip not in all_ips or end_ip not in all_ips:
error = 'Start and End IP must be within the subnet.'
else:
user_name = get_current_user_name()
if 'remove' in request.form:
cursor.execute('DELETE FROM DHCPPool WHERE subnet_id = %s', (subnet_id,))
cursor.execute('UPDATE IPAddress SET hostname=NULL WHERE subnet_id=%s AND hostname="DHCP"', (subnet_id,))
if dhcp_pool:
cursor.execute('''UPDATE DHCPPool SET start_ip = %s, end_ip = %s, excluded_ips = %s WHERE subnet_id = %s''', (start_ip, end_ip, excluded_ips, subnet_id))
action = 'dhcp_pool_update'
details = f"Updated DHCP pool for subnet {subnet[1]} ({subnet[2]}): {start_ip} - {end_ip}, excluded: {excluded_ips}"
else:
cursor.execute('''INSERT INTO DHCPPool (subnet_id, start_ip, end_ip, excluded_ips) VALUES (%s, %s, %s, %s)''', (subnet_id, start_ip, end_ip, excluded_ips))
action = 'dhcp_pool_create'
details = f"Created DHCP pool for subnet {subnet[1]} ({subnet[2]}): {start_ip} - {end_ip}, excluded: {excluded_ips}"
in_range = False
for ip in all_ips:
if ip == start_ip:
in_range = True
if in_range and ip not in excluded_list:
cursor.execute('UPDATE IPAddress SET hostname="DHCP" WHERE subnet_id=%s AND ip=%s', (subnet_id, ip))
if ip == end_ip:
break
conn.commit()
dhcp_pool = {'start_ip': start_ip, 'end_ip': end_ip, 'excluded_ips': excluded_ips}
add_audit_log(session['user_id'], action, details, subnet_id, conn=conn)
dhcp_pool = None
add_audit_log(session['user_id'], 'dhcp_pool_remove', f"Removed DHCP pool for subnet {subnet[1]} ({subnet[2]})", subnet_id, conn=conn)
else:
start_ip = request.form['start_ip']
end_ip = request.form['end_ip']
excluded_ips = request.form.get('excluded_ips', '').replace(' ', '')
excluded_list = [ip for ip in excluded_ips.split(',') if ip]
cursor.execute('SELECT ip FROM IPAddress WHERE subnet_id = %s', (subnet_id,))
all_ips = [row[0] for row in cursor.fetchall()]
if start_ip not in all_ips or end_ip not in all_ips:
error = 'Start and End IP must be within the subnet.'
else:
cursor.execute('UPDATE IPAddress SET hostname=NULL WHERE subnet_id=%s AND hostname="DHCP"', (subnet_id,))
if dhcp_pool:
cursor.execute('''UPDATE DHCPPool SET start_ip = %s, end_ip = %s, excluded_ips = %s WHERE subnet_id = %s''', (start_ip, end_ip, excluded_ips, subnet_id))
action = 'dhcp_pool_update'
details = f"Updated DHCP pool for subnet {subnet[1]} ({subnet[2]}): {start_ip} - {end_ip}, excluded: {excluded_ips}"
else:
cursor.execute('''INSERT INTO DHCPPool (subnet_id, start_ip, end_ip, excluded_ips) VALUES (%s, %s, %s, %s)''', (subnet_id, start_ip, end_ip, excluded_ips))
action = 'dhcp_pool_create'
details = f"Created DHCP pool for subnet {subnet[1]} ({subnet[2]}): {start_ip} - {end_ip}, excluded: {excluded_ips}"
in_range = False
for ip in all_ips:
if ip == start_ip:
in_range = True
if in_range and ip not in excluded_list:
cursor.execute('UPDATE IPAddress SET hostname="DHCP" WHERE subnet_id=%s AND ip=%s', (subnet_id, ip))
if ip == end_ip:
break
conn.commit()
dhcp_pool = {'start_ip': start_ip, 'end_ip': end_ip, 'excluded_ips': excluded_ips}
add_audit_log(session['user_id'], action, details, subnet_id, conn=conn)
return render_with_user('dhcp.html', subnet={'id': subnet[0], 'name': subnet[1]}, dhcp_pool=dhcp_pool, error=error)
@app.route('/device_type_stats')
@login_required
@permission_required('view_device_type_stats')
def device_type_stats():
from flask import current_app
with get_db_connection(current_app) as conn:
@@ -603,7 +795,7 @@ def register_routes(app):
return render_with_user('device_type_stats.html', stats=stats)
@app.route('/device_types', methods=['GET', 'POST'])
@login_required
@permission_required('view_device_types')
def device_types():
from flask import current_app
error = None
@@ -613,61 +805,70 @@ def register_routes(app):
action = request.form['action']
user_name = get_current_user_name()
if action == 'add':
name = request.form['name'].strip()
icon_class = request.form['icon_class'].strip()
if not name:
error = 'Device type name is required.'
elif not icon_class:
error = 'Icon class is required.'
if not has_permission('add_device_type', conn=conn):
error = 'You do not have permission to add device types.'
else:
try:
cursor.execute('INSERT INTO DeviceType (name, icon_class) VALUES (%s, %s)', (name, icon_class))
conn.commit()
logging.info(f"User {user_name} added device type '{name}' with icon '{icon_class}'.")
except mysql.connector.IntegrityError as e:
if e.errno == 1062: # Duplicate entry
error = f"Device type '{name}' already exists."
else:
raise
name = request.form['name'].strip()
icon_class = request.form['icon_class'].strip()
if not name:
error = 'Device type name is required.'
elif not icon_class:
error = 'Icon class is required.'
else:
try:
cursor.execute('INSERT INTO DeviceType (name, icon_class) VALUES (%s, %s)', (name, icon_class))
conn.commit()
logging.info(f"User {user_name} added device type '{name}' with icon '{icon_class}'.")
except mysql.connector.IntegrityError as e:
if e.errno == 1062: # Duplicate entry
error = f"Device type '{name}' already exists."
else:
raise
elif action == 'edit':
device_type_id = request.form['device_type_id']
name = request.form['name'].strip()
icon_class = request.form['icon_class'].strip()
if not name:
error = 'Device type name is required.'
elif not icon_class:
error = 'Icon class is required.'
if not has_permission('edit_device_type', conn=conn):
error = 'You do not have permission to edit device types.'
else:
try:
cursor.execute('UPDATE DeviceType SET name = %s, icon_class = %s WHERE id = %s', (name, icon_class, device_type_id))
conn.commit()
logging.info(f"User {user_name} edited device type {device_type_id} to '{name}' with icon '{icon_class}'.")
except mysql.connector.IntegrityError as e:
if e.errno == 1062: # Duplicate entry
error = f"Device type '{name}' already exists."
else:
raise
device_type_id = request.form['device_type_id']
name = request.form['name'].strip()
icon_class = request.form['icon_class'].strip()
if not name:
error = 'Device type name is required.'
elif not icon_class:
error = 'Icon class is required.'
else:
try:
cursor.execute('UPDATE DeviceType SET name = %s, icon_class = %s WHERE id = %s', (name, icon_class, device_type_id))
conn.commit()
logging.info(f"User {user_name} edited device type {device_type_id} to '{name}' with icon '{icon_class}'.")
except mysql.connector.IntegrityError as e:
if e.errno == 1062: # Duplicate entry
error = f"Device type '{name}' already exists."
else:
raise
elif action == 'delete':
device_type_id = request.form['device_type_id']
# Check if any devices are using this device type
cursor.execute('SELECT COUNT(*) FROM Device WHERE device_type_id = %s', (device_type_id,))
device_count = cursor.fetchone()[0]
if device_count > 0:
cursor.execute('SELECT name FROM DeviceType WHERE id = %s', (device_type_id,))
device_type_name = cursor.fetchone()[0]
error = f"Cannot delete device type '{device_type_name}' because {device_count} device(s) are using it."
if not has_permission('delete_device_type', conn=conn):
error = 'You do not have permission to delete device types.'
else:
cursor.execute('SELECT name FROM DeviceType WHERE id = %s', (device_type_id,))
device_type_name = cursor.fetchone()[0]
cursor.execute('DELETE FROM DeviceType WHERE id = %s', (device_type_id,))
conn.commit()
logging.info(f"User {user_name} deleted device type '{device_type_name}'.")
device_type_id = request.form['device_type_id']
# Check if any devices are using this device type
cursor.execute('SELECT COUNT(*) FROM Device WHERE device_type_id = %s', (device_type_id,))
device_count = cursor.fetchone()[0]
if device_count > 0:
cursor.execute('SELECT name FROM DeviceType WHERE id = %s', (device_type_id,))
device_type_name = cursor.fetchone()[0]
error = f"Cannot delete device type '{device_type_name}' because {device_count} device(s) are using it."
else:
cursor.execute('SELECT name FROM DeviceType WHERE id = %s', (device_type_id,))
device_type_name = cursor.fetchone()[0]
cursor.execute('DELETE FROM DeviceType WHERE id = %s', (device_type_id,))
conn.commit()
logging.info(f"User {user_name} deleted device type '{device_type_name}'.")
cursor.execute('SELECT id, name, icon_class FROM DeviceType ORDER BY name')
device_types = cursor.fetchall()
return render_with_user('device_types.html', device_types=device_types, error=error)
@app.route('/devices/type/<device_type>')
@login_required
@permission_required('view_devices_by_type')
def devices_by_type(device_type):
from flask import current_app
with get_db_connection(current_app) as conn:
@@ -699,7 +900,7 @@ def register_routes(app):
return render_with_user('devices_by_type.html', device_type=device_type, icon_class=icon_class, site_devices=site_devices)
@app.route('/racks')
@login_required
@permission_required('view_racks')
def racks():
from flask import current_app
with get_db_connection(current_app) as conn:
@@ -719,7 +920,7 @@ def register_routes(app):
return render_with_user('racks.html', racks=racks)
@app.route('/rack/add', methods=['GET', 'POST'])
@login_required
@permission_required('add_rack')
def add_rack():
from flask import current_app
if request.method == 'POST':
@@ -738,7 +939,7 @@ def register_routes(app):
return render_with_user('add_rack.html')
@app.route('/rack/<int:rack_id>')
@login_required
@permission_required('view_rack')
def rack(rack_id):
from flask import current_app, request
side = request.args.get('side', 'front')
@@ -775,7 +976,7 @@ def register_routes(app):
return render_with_user('rack.html', rack=rack, rack_devices=rack_devices, site_devices=site_devices, current_side=side)
@app.route('/rack/<int:rack_id>/add_device', methods=['POST'])
@login_required
@permission_required('add_device_to_rack')
def rack_add_device(rack_id):
device_id = int(request.form['device_id'])
position_u = int(request.form['position_u'])
@@ -835,7 +1036,7 @@ def register_routes(app):
return redirect(url_for('rack', rack_id=rack_id))
@app.route('/rack/<int:rack_id>/add_nonnet_device', methods=['POST'])
@login_required
@permission_required('add_nonnet_device_to_rack')
def rack_add_nonnet_device(rack_id):
device_name = request.form['device_name']
position_u = int(request.form['position_u'])
@@ -894,7 +1095,7 @@ def register_routes(app):
return redirect(url_for('rack', rack_id=rack_id))
@app.route('/rack/<int:rack_id>/remove_device', methods=['POST'])
@login_required
@permission_required('remove_device_from_rack')
def rack_remove_device(rack_id):
rack_device_id = int(request.form['rack_device_id'])
user_name = get_current_user_name()
@@ -919,7 +1120,7 @@ def register_routes(app):
return redirect(url_for('rack', rack_id=rack_id))
@app.route('/rack/<int:rack_id>/delete', methods=['POST'])
@login_required
@permission_required('delete_rack')
def delete_rack(rack_id):
user_name = get_current_user_name()
from flask import current_app
@@ -934,7 +1135,7 @@ def register_routes(app):
return redirect(url_for('racks'))
@app.route('/rack/<int:rack_id>/export_csv')
@login_required
@permission_required('export_rack_csv')
def export_rack_csv(rack_id):
from flask import current_app
with get_db_connection(current_app) as conn:
@@ -983,7 +1184,7 @@ def register_routes(app):
)
@app.route('/help')
@login_required
@permission_required('view_help')
def help():
return render_with_user('help.html')
@@ -1014,6 +1215,7 @@ def register_routes(app):
app.add_url_rule('/delete_device', 'delete_device', delete_device, methods=['POST'])
app.add_url_rule('/subnet/<int:subnet_id>', 'subnet', subnet)
app.add_url_rule('/add_subnet', 'add_subnet', add_subnet, methods=['POST'])
app.add_url_rule('/edit_subnet', 'edit_subnet', edit_subnet, methods=['POST'])
app.add_url_rule('/delete_subnet', 'delete_subnet', delete_subnet, methods=['POST'])
app.add_url_rule('/admin', 'admin', admin, methods=['GET', 'POST'])
app.add_url_rule('/users', 'users', users, methods=['GET', 'POST'])
+210 -34
View File
@@ -10,44 +10,220 @@
</head>
<body class="bg-gray-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 min-h-screen flex flex-col">
{% include 'header.html' %}
<div class="flex-1 flex items-center justify-center mx-4">
<div class="container py-8 max-w-md pt-20">
<div class="bg-gray-200 dark:bg-zinc-800 rounded-2xl shadow-lg p-8">
<h1 class="text-3xl font-bold mb-6 text-center">Admin</h1>
<div class="flex justify-center gap-4 mb-6">
<a href="/audit" class="bg-gray-300 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 px-4 py-2 rounded-lg shadow text-center w-40">Audit Log</a>
<a href="/users" class="bg-gray-300 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 px-4 py-2 rounded-lg shadow text-center w-40">Users</a>
</div>
<hr class="border-t-2 border-gray-600 rounded-lg mb-4">
<h1 class="text-2xl font-bold mb-6 text-center">Add Subnet</h1>
<div class="flex-1 mx-4 py-8 pt-20">
<div class="container max-w-7xl mx-auto">
<h1 class="text-3xl font-bold mb-6 text-center">Admin Panel</h1>
{% if error %}
<div class="text-red-500 text-center mb-4">{{ error }}</div>
<div class="bg-red-200 dark:bg-red-800 text-red-800 dark:text-red-200 p-4 rounded-lg mb-6">
{{ error }}
</div>
{% endif %}
<form action="/add_subnet" method="POST" class="mb-6" onsubmit="return validateSubnetForm();">
<div class="flex flex-col space-y-4">
<input type="text" name="name" placeholder="Subnet Name" class="border p-3 rounded-lg bg-gray-300 dark:bg-zinc-900 border-gray-600" required>
<input type="text" name="cidr" id="cidr-input" placeholder="CIDR (e.g., 192.168.1.0/24)" class="border p-3 rounded-lg bg-gray-300 dark:bg-zinc-900 border-gray-600" required>
<input type="text" name="site" placeholder="Site/Location" class="border p-3 rounded-lg bg-gray-300 dark:bg-zinc-900 border-gray-600" required>
<button type="submit" class="bg-gray-300 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer px-4 py-2 rounded-lg">Add Subnet</button>
<span id="cidr-error" class="text-red-500 text-sm hidden"></span>
<!-- Quick Links -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-8">
<a href="/audit" class="bg-gray-200 dark:bg-zinc-800 hover:bg-gray-300 dark:hover:bg-zinc-700 p-6 rounded-lg shadow-md flex items-center justify-between transition-colors">
<div class="flex items-center space-x-4">
<i class="fas fa-clipboard-list text-3xl text-blue-500"></i>
<div>
<h3 class="text-lg font-bold">Audit Log</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">View system activity</p>
</div>
</div>
<i class="fas fa-chevron-right text-gray-400"></i>
</a>
<a href="/users" class="bg-gray-200 dark:bg-zinc-800 hover:bg-gray-300 dark:hover:bg-zinc-700 p-6 rounded-lg shadow-md flex items-center justify-between transition-colors">
<div class="flex items-center space-x-4">
<i class="fas fa-users text-3xl text-green-500"></i>
<div>
<h3 class="text-lg font-bold">User Management</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">Manage users & roles</p>
</div>
</div>
<i class="fas fa-chevron-right text-gray-400"></i>
</a>
</div>
<!-- Subnet Management Section -->
<div class="bg-gray-200 dark:bg-zinc-800 p-6 rounded-lg shadow-md">
<div class="flex justify-between items-center mb-6">
<h2 class="text-2xl font-bold">Subnet Management</h2>
{% if can_add_subnet %}
<button onclick="showAddSubnetModal()" class="bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer px-6 py-2 rounded-lg font-medium">
<i class="fas fa-plus mr-2"></i>Add Subnet
</button>
{% endif %}
</div>
</form>
<hr class="border-t-2 border-gray-600 rounded-lg mb-4">
<h1 class="text-2xl font-bold mb-6 text-center">Delete Subnet</h1>
<form action="/delete_subnet" method="POST" class="mb-6 flex items-center space-x-4 justify-center" onsubmit="return confirm('Are you sure you want to delete this subnet and all its IPs? This action is irreversible.');">
<select name="subnet_id" class="border p-3 rounded-lg bg-gray-300 dark:bg-zinc-900 border-gray-600" required>
<option value="" disabled selected>Select Subnet</option>
{% for subnet in subnets %}
<option value="{{ subnet.id }}">{{ subnet.name }} ({{ subnet.cidr }})</option>
{% endfor %}
</select>
<button type="submit" class="text-red-500 hover:text-red-700 hover:cursor-pointer rounded-full p-3" title="Delete Subnet">
<i class="fas fa-trash fa-lg"></i>
</button>
</form>
</div>
{% if subnets %}
<div class="overflow-x-auto">
<table class="w-full">
<thead>
<tr class="border-b border-gray-600">
<th class="text-left p-3">Name</th>
<th class="text-left p-3">CIDR</th>
<th class="text-left p-3">Site</th>
<th class="text-left p-3">Actions</th>
</tr>
</thead>
<tbody>
{% for subnet in subnets %}
<tr class="border-b border-gray-600 hover:bg-gray-300 dark:hover:bg-zinc-700">
<td class="p-3 font-medium">{{ subnet.name }}</td>
<td class="p-3 font-mono text-sm">{{ subnet.cidr }}</td>
<td class="p-3">
<span class="px-2 py-1 bg-blue-200 dark:bg-blue-800 rounded text-sm">{{ subnet.site }}</span>
</td>
<td class="p-3">
<div class="flex items-center space-x-2">
<a href="/subnet/{{ subnet.id }}" class="text-blue-500 hover:text-blue-700" title="View Subnet">
<i class="fas fa-eye"></i>
</a>
{% if can_edit_subnet %}
<button onclick="editSubnet({{ subnet.id }}, '{{ subnet.name|replace("'", "\\'") }}', '{{ subnet.cidr|replace("'", "\\'") }}', '{{ subnet.site|replace("'", "\\'") }}')" class="text-green-500 hover:text-green-700" title="Edit Subnet">
<i class="fas fa-edit"></i>
</button>
{% endif %}
{% if can_delete_subnet %}
<form action="/delete_subnet" method="POST" onsubmit="return confirm('Are you sure you want to delete this subnet and all its IPs? This action is irreversible.');" class="inline">
<input type="hidden" name="subnet_id" value="{{ subnet.id }}">
<button type="submit" class="text-red-500 hover:text-red-700" title="Delete Subnet">
<i class="fas fa-trash"></i>
</button>
</form>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-8 text-gray-500">
<i class="fas fa-network-wired text-4xl mb-4"></i>
<p>No subnets found. Add your first subnet to get started.</p>
</div>
{% endif %}
</div>
</div>
</div>
<!-- Add Subnet Modal -->
<div id="add-subnet-modal" class="hidden fixed inset-0 bg-black/30 backdrop-blur-md flex items-center justify-center z-50">
<div class="bg-gray-200 dark:bg-zinc-800 p-6 rounded-lg shadow-lg max-w-md w-full mx-4">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold">Add New Subnet</h2>
<button onclick="closeAddSubnetModal()" class="text-gray-500 hover:text-gray-700">
<i class="fas fa-times"></i>
</button>
</div>
<form action="/add_subnet" method="POST" onsubmit="return validateSubnetForm();">
<div class="space-y-4">
<input type="text" name="name" id="add-subnet-name" placeholder="Subnet Name" class="border p-3 rounded-lg bg-gray-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 border-gray-600 w-full" required>
<input type="text" name="cidr" id="add-subnet-cidr" placeholder="CIDR (e.g., 192.168.1.0/24)" class="border p-3 rounded-lg bg-gray-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 border-gray-600 w-full" required>
<input type="text" name="site" id="add-subnet-site" placeholder="Site/Location" class="border p-3 rounded-lg bg-gray-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 border-gray-600 w-full" required>
<span id="cidr-error" class="text-red-500 text-sm hidden"></span>
</div>
<div class="flex justify-end space-x-2 mt-6">
<button type="button" onclick="closeAddSubnetModal()" class="px-4 py-2 bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer rounded-lg">Cancel</button>
<button type="submit" class="px-4 py-2 bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer rounded-lg">Add Subnet</button>
</div>
</form>
</div>
</div>
<!-- Edit Subnet Modal -->
<div id="edit-subnet-modal" class="hidden fixed inset-0 bg-black/30 backdrop-blur-md flex items-center justify-center z-50">
<div class="bg-gray-200 dark:bg-zinc-800 p-6 rounded-lg shadow-lg max-w-md w-full mx-4">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold">Edit Subnet</h2>
<button onclick="closeEditSubnetModal()" class="text-gray-500 hover:text-gray-700">
<i class="fas fa-times"></i>
</button>
</div>
<form action="/edit_subnet" method="POST" onsubmit="return validateEditSubnetForm();">
<input type="hidden" name="subnet_id" id="edit-subnet-id">
<div class="space-y-4">
<input type="text" name="name" id="edit-subnet-name" placeholder="Subnet Name" class="border p-3 rounded-lg bg-gray-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 border-gray-600 w-full" required>
<input type="text" name="cidr" id="edit-subnet-cidr" placeholder="CIDR (e.g., 192.168.1.0/24)" class="border p-3 rounded-lg bg-gray-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 border-gray-600 w-full" required>
<input type="text" name="site" id="edit-subnet-site" placeholder="Site/Location" class="border p-3 rounded-lg bg-gray-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 border-gray-600 w-full" required>
<span id="edit-cidr-error" class="text-red-500 text-sm hidden"></span>
</div>
<div class="flex justify-end space-x-2 mt-6">
<button type="button" onclick="closeEditSubnetModal()" class="px-4 py-2 bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer rounded-lg">Cancel</button>
<button type="submit" class="px-4 py-2 bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer rounded-lg">Save Changes</button>
</div>
</form>
</div>
</div>
<script src="/static/js/add_subnet.js"></script>
<script>
function showAddSubnetModal() {
document.getElementById('add-subnet-modal').classList.remove('hidden');
document.getElementById('add-subnet-name').value = '';
document.getElementById('add-subnet-cidr').value = '';
document.getElementById('add-subnet-site').value = '';
}
function closeAddSubnetModal() {
document.getElementById('add-subnet-modal').classList.add('hidden');
document.getElementById('cidr-error').classList.add('hidden');
}
function editSubnet(subnetId, name, cidr, site) {
document.getElementById('edit-subnet-id').value = subnetId;
document.getElementById('edit-subnet-name').value = name;
document.getElementById('edit-subnet-cidr').value = cidr;
document.getElementById('edit-subnet-site').value = site;
document.getElementById('edit-subnet-modal').classList.remove('hidden');
}
function closeEditSubnetModal() {
document.getElementById('edit-subnet-modal').classList.add('hidden');
document.getElementById('edit-cidr-error').classList.add('hidden');
}
function validateEditSubnetForm() {
const cidrInput = document.getElementById('edit-subnet-cidr');
const cidrError = document.getElementById('edit-cidr-error');
const cidr = cidrInput.value.trim();
// Basic CIDR validation
const cidrPattern = /^(\d{1,3}\.){3}\d{1,3}\/\d{1,2}$/;
if (!cidrPattern.test(cidr)) {
cidrError.textContent = 'Invalid CIDR format. Use format like 192.168.1.0/24';
cidrError.classList.remove('hidden');
return false;
}
// Check prefix length
const parts = cidr.split('/');
if (parts.length === 2) {
const prefixLen = parseInt(parts[1]);
if (prefixLen < 24 || prefixLen > 32) {
cidrError.textContent = 'Subnet must be /24 or smaller (e.g., /24, /25, ... /32)';
cidrError.classList.remove('hidden');
return false;
}
}
cidrError.classList.add('hidden');
return true;
}
// Close modals when clicking outside
window.onclick = function(event) {
const addModal = document.getElementById('add-subnet-modal');
const editModal = document.getElementById('edit-subnet-modal');
if (event.target === addModal) {
closeAddSubnetModal();
}
if (event.target === editModal) {
closeEditSubnetModal();
}
}
</script>
</body>
</html>
</html>
+30
View File
@@ -7,12 +7,27 @@
{% if current_user_name %}Hello, {{ current_user_name }}{% endif %}
</div>
<nav class="hidden md:flex items-center space-x-6" id="main-nav">
{% if has_permission('view_index') %}
<a href="/" class="text-gray-200 hover:text-gray-400 font-medium">Home</a>
{% endif %}
{% if has_permission('view_devices') %}
<a href="/devices" class="text-gray-200 hover:text-gray-400 font-medium">Devices</a>
{% endif %}
{% if has_permission('view_racks') %}
<a href="/racks" class="text-gray-200 hover:text-gray-400 font-medium">Racks</a>
{% endif %}
{% if has_permission('view_admin') %}
<a href="/admin" class="text-gray-200 hover:text-gray-400 font-medium">Admin</a>
{% endif %}
{% if has_permission('view_users') %}
<a href="/users" class="text-gray-200 hover:text-gray-400 font-medium">Users</a>
{% endif %}
{% if has_permission('view_audit') %}
<a href="/audit" class="text-gray-200 hover:text-gray-400 font-medium">Audit Log</a>
{% endif %}
{% if has_permission('view_help') %}
<a href="/help" class="text-gray-200 hover:text-gray-400 font-medium">Help</a>
{% endif %}
{% if current_user_name %}
<a href="/logout" class="text-gray-200 hover:text-gray-400 font-medium">Logout</a>
{% endif %}
@@ -23,12 +38,27 @@
</svg>
</button>
<div class="md:hidden absolute top-16 right-6 bg-zinc-800 rounded-lg shadow-lg z-50 w-48 hidden flex-col py-2" id="mobile-nav">
{% if has_permission('view_index') %}
<a href="/" class="block px-6 py-2 text-gray-200 hover:text-gray-400 font-medium">Home</a>
{% endif %}
{% if has_permission('view_devices') %}
<a href="/devices" class="block px-6 py-2 text-gray-200 hover:text-gray-400 font-medium">Devices</a>
{% endif %}
{% if has_permission('view_racks') %}
<a href="/racks" class="block px-6 py-2 text-gray-200 hover:text-gray-400 font-medium">Racks</a>
{% endif %}
{% if has_permission('view_admin') %}
<a href="/admin" class="block px-6 py-2 text-gray-200 hover:text-gray-400 font-medium">Admin</a>
{% endif %}
{% if has_permission('view_users') %}
<a href="/users" class="block px-6 py-2 text-gray-200 hover:text-gray-400 font-medium">Users</a>
{% endif %}
{% if has_permission('view_audit') %}
<a href="/audit" class="block px-6 py-2 text-gray-200 hover:text-gray-400 font-medium">Audit Log</a>
{% endif %}
{% if has_permission('view_help') %}
<a href="/help" class="block px-6 py-2 text-gray-200 hover:text-gray-400 font-medium">Help</a>
{% endif %}
{% if current_user_name %}
<a href="/logout" class="block px-6 py-2 text-gray-200 hover:text-gray-400 font-medium">Logout</a>
{% endif %}
+21 -4
View File
@@ -19,7 +19,7 @@
<div class="space-y-4">
<div>
<h3 class="text-xl font-semibold mb-1">Managing Subnets</h3>
<p>To add or remove subnets, go to the <a href="/admin" class="text-blue-600 dark:text-blue-400 hover:underline">Admin</a> page. Click <span class="bg-gray-300 dark:bg-zinc-700 px-2 py-1 rounded">Add Subnet</span> to create a new subnet. Subnets are associated with sites.</p>
<p>To add or edit subnets, go to the <a href="/admin" class="text-blue-600 dark:text-blue-400 hover:underline">Admin</a> page. Click <span class="bg-gray-300 dark:bg-zinc-700 px-2 py-1 rounded">Add Subnet</span> to create a new subnet, or use the edit button to modify existing subnets. Subnets are associated with sites and can be organised by location.</p>
</div>
<div>
<h3 class="text-xl font-semibold mb-1">Adding a Device</h3>
@@ -56,12 +56,29 @@
<h2 class="text-2xl font-semibold mb-4 border-b border-gray-700 pb-2">User Management & Audit</h2>
<div class="space-y-4">
<div>
<h3 class="text-xl font-semibold mb-1">User Management & Admin</h3>
<p>Users can manage themselves and other users from the <a href="/users" class="text-blue-600 dark:text-blue-400 hover:underline">Users</a> page. Use this area to add, remove, or update user accounts.</p>
<h3 class="text-xl font-semibold mb-1">User & Role Management</h3>
<p>Administrators can manage users and roles from the <a href="/users" class="text-blue-600 dark:text-blue-400 hover:underline">Users</a> page. This includes creating users, assigning roles, and managing custom roles with specific permission sets. Only users with the appropriate permissions can access this page.</p>
</div>
<div>
<h3 class="text-xl font-semibold mb-1">Understanding Roles</h3>
<p>The system uses role-based access control to manage what users can do. There are three default roles:</p>
<ul class="list-disc list-inside mt-2 space-y-1 ml-4">
<li><strong>Admin:</strong> Full access to all features including user and role management</li>
<li><strong>User:</strong> Can view and manage most features (devices, subnets, racks, etc.) but cannot manage users or roles</li>
<li><strong>View Only:</strong> Read-only access to view pages but cannot make any changes</li>
</ul>
</div>
<div>
<h3 class="text-xl font-semibold mb-1">Custom Roles</h3>
<p>Administrators can create custom roles with specific permission sets. Go to the <a href="/users" class="text-blue-600 dark:text-blue-400 hover:underline">Users</a> page and click the "Roles & Permissions" tab to create and manage roles.</p>
</div>
<div>
<h3 class="text-xl font-semibold mb-1">Permission Granularity</h3>
<p>Permissions are very granular, allowing fine-grained control over what each role can do. Permissions are organised into categories like View, Device Management, Network Management, Rack Management, and Administration.</p>
</div>
<div>
<h3 class="text-xl font-semibold mb-1">Audit & History</h3>
<p>All changes are logged and can be reviewed on the <a href="/audit" class="text-blue-600 dark:text-blue-400 hover:underline">Audit</a> page for accountability and troubleshooting.</p>
<p>All changes are logged and can be reviewed on the <a href="/audit" class="text-blue-600 dark:text-blue-400 hover:underline">Audit</a> page for accountability and troubleshooting. The audit log shows who made changes, what was changed, and when.</p>
</div>
</div>
</div>
+511 -32
View File
@@ -3,46 +3,525 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>User Management</title>
<title>User & Role Management</title>
<link rel="icon" type="image/png" href="{{ LOGO_PNG }}">
<link href="/static/css/output.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css" rel="stylesheet">
</head>
<body class="bg-gray-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 min-h-screen flex flex-col">
{% include 'header.html' %}
<div class="flex-1 flex items-center justify-center mx-4">
<div class="container py-8 max-w-6xl pt-20">
<h1 class="text-3xl font-bold mb-6 text-center">User Management</h1>
<form action="/users" method="POST" class="mb-8 bg-gray-200 dark:bg-zinc-800 p-6 rounded-lg shadow-md">
<input type="hidden" name="action" value="add">
<div class="flex flex-col space-y-4 items-center w-full">
<input type="text" name="name" placeholder="Name" class="border p-3 rounded-lg bg-gray-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 border-gray-600 w-full" required>
<input type="email" name="email" placeholder="Email Address" class="border p-3 rounded-lg bg-gray-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 border-gray-600 w-full" required>
<input type="password" name="password" placeholder="Password" class="border p-3 rounded-lg bg-gray-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 border-gray-600 w-full" required>
<button type="submit" class="bg-gray-300 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer px-4 py-2 rounded-lg max-w-md w-full sm:w-auto">Add User</button>
<div class="flex-1 mx-4 py-8 pt-20">
<div class="container max-w-7xl mx-auto">
<h1 class="text-3xl font-bold mb-6 text-center">User & Role Management</h1>
{% if error %}
<div class="bg-red-200 dark:bg-red-800 text-red-800 dark:text-red-200 p-4 rounded-lg mb-6">
{{ error }}
</div>
{% endif %}
<!-- Tabs -->
<div class="mb-6 border-b border-gray-600">
<button onclick="showTab('users')" id="tab-users" class="tab-button px-6 py-3 font-medium border-b-2 border-blue-500 text-blue-600 dark:text-blue-400 hover:text-gray-800 dark:hover:text-gray-200 hover:cursor-pointer">
Users
</button>
<button onclick="showTab('roles')" id="tab-roles" class="tab-button px-6 py-3 font-medium border-b-2 border-transparent text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 hover:cursor-pointer">
Roles & Permissions
</button>
</div>
<!-- Users Tab -->
<div id="users-tab" class="tab-content">
{% if can_manage_users %}
<div class="bg-gray-200 dark:bg-zinc-800 p-6 rounded-lg shadow-md mb-8">
<h2 class="text-xl font-bold mb-4">Add New User</h2>
<form action="/users" method="POST" class="space-y-4">
<input type="hidden" name="action" value="add_user">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<input type="text" name="name" placeholder="Name" class="border p-3 rounded-lg bg-gray-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 border-gray-600" required>
<input type="email" name="email" placeholder="Email Address" class="border p-3 rounded-lg bg-gray-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 border-gray-600" required>
<input type="password" name="password" placeholder="Password" class="border p-3 rounded-lg bg-gray-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 border-gray-600" required>
<select name="role_id" class="border p-3 rounded-lg bg-gray-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 border-gray-600">
<option value="">No Role</option>
{% for role in roles %}
<option value="{{ role[0] }}">{{ role[1] }}</option>
{% endfor %}
</select>
</div>
<button type="submit" class="bg-gray-300 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer px-4 py-2 rounded-lg">Add User</button>
</form>
</div>
</form>
<h2 class="text-xl font-bold mb-4">Existing Users</h2>
<ul class="space-y-4">
{% for user in users %}
<li class="bg-gray-200 dark:bg-zinc-800 p-4 rounded-lg flex justify-between items-center">
<form action="/users" method="POST" class="flex flex-row items-center space-x-2">
<input type="hidden" name="action" value="edit">
<input type="hidden" name="user_id" value="{{ user[0] }}">
<input type="text" name="name" value="{{ user[1] }}" class="border p-2 rounded-lg bg-gray-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 border-gray-600 w-52">
<input type="email" name="email" value="{{ user[2] }}" class="border p-2 rounded-lg bg-gray-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 border-gray-600 w-80">
<input type="password" name="password" placeholder="New Password (leave blank to keep)" class="border p-2 rounded-lg bg-gray-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 border-gray-600 w-80">
<button type="submit" class="bg-gray-300 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer px-3 py-1 rounded-lg">Save</button>
</form>
<form action="/users" method="POST" onsubmit="return confirm('Are you sure you want to delete this user?');">
<input type="hidden" name="action" value="delete">
<input type="hidden" name="user_id" value="{{ user[0] }}">
<button type="submit" class="text-red-500 hover:text-red-700 hover:cursor-pointer mx-4" title="Delete User"><i class="fas fa-trash"></i></button>
</form>
</li>
{% endfor %}
</ul>
{% endif %}
<h2 class="text-xl font-bold mb-4">Existing Users</h2>
<div class="bg-gray-200 dark:bg-zinc-800 p-6 rounded-lg shadow-md">
<div class="overflow-x-auto">
<table class="w-full">
<thead>
<tr class="border-b border-gray-600">
<th class="text-left p-2">Name</th>
<th class="text-left p-2">Email</th>
<th class="text-left p-2">Role</th>
{% if can_manage_users %}
<th class="text-left p-2">Actions</th>
{% endif %}
</tr>
</thead>
<tbody>
{% for user in users %}
<tr class="border-b border-gray-600">
<td class="p-2">{{ user[1] }}</td>
<td class="p-2">{{ user[2] }}</td>
<td class="p-2">
<span class="px-2 py-1 bg-blue-200 dark:bg-blue-800 rounded">{{ user[4] or 'No Role' }}</span>
</td>
{% if can_manage_users %}
<td class="p-2">
<button onclick="editUser({{ user[0] }}, '{{ user[1]|replace("'", "\\'") }}', '{{ user[2]|replace("'", "\\'") }}', {{ user[3] if user[3] else 'null' }})" class="text-blue-500 hover:text-blue-700 mr-4 hover:cursor-pointer" title="Edit User">
<i class="fas fa-edit"></i>
</button>
<form action="/users" method="POST" onsubmit="return confirm('Are you sure you want to delete this user?');" class="inline">
<input type="hidden" name="action" value="delete_user">
<input type="hidden" name="user_id" value="{{ user[0] }}">
<button type="submit" class="text-red-500 hover:text-red-700 hover:cursor-pointer" title="Delete User">
<i class="fas fa-trash"></i>
</button>
</form>
</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<!-- Roles Tab -->
<div id="roles-tab" class="tab-content hidden">
{% if can_manage_roles %}
<div class="mb-6 flex justify-end">
<button type="button" onclick="showAddRoleModal(); return false;" class="bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer px-6 py-2 rounded-lg font-medium">
<i class="fas fa-plus mr-2"></i>Add New Role
</button>
</div>
{% endif %}
<h2 class="text-xl font-bold mb-4">Roles</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{% for role in roles %}
<div class="bg-gray-200 dark:bg-zinc-800 p-6 rounded-lg shadow-md hover:shadow-lg transition-shadow">
<div class="flex justify-between items-start mb-3">
<div class="flex-1">
<h3 class="text-lg font-bold mb-1">{{ role[1] }}</h3>
{% if role[2] %}
<p class="text-sm text-gray-600 dark:text-gray-400 mb-3">{{ role[2] }}</p>
{% endif %}
</div>
{% if can_manage_roles %}
<div class="flex space-x-2">
<button type="button" onclick="editRole({{ role[0] }}, '{{ role[1]|replace("'", "\\'") }}', '{{ (role[2] or '')|replace("'", "\\'") }}'); return false;" class="text-blue-500 hover:text-blue-700 hover:cursor-pointer" title="Edit Role">
<i class="fas fa-edit"></i>
</button>
<button type="button" onclick="deleteRole({{ role[0] }}, '{{ role[1]|replace("'", "\\'") }}'); return false;" class="text-red-500 hover:text-red-700 hover:cursor-pointer" title="Delete Role">
<i class="fas fa-trash"></i>
</button>
</div>
{% endif %}
</div>
<div>
<p class="text-xs text-gray-500 dark:text-gray-400 mb-2">Permissions:</p>
{% set role_perms = role_permissions.get(role[0], []) %}
{% set perm_dict = {} %}
{% for perm in permissions %}
{% set _ = perm_dict.update({perm[0]: perm[2]}) %}
{% endfor %}
<div class="flex flex-wrap gap-1">
{% for perm_id in role_perms[:5] %}
<span class="px-2 py-1 bg-green-200 dark:bg-green-800 rounded text-xs">{{ perm_dict.get(perm_id, 'Unknown') }}</span>
{% endfor %}
{% if role_perms|length > 5 %}
<span class="px-2 py-1 bg-gray-300 dark:bg-gray-700 rounded text-xs">+{{ role_perms|length - 5 }} more</span>
{% endif %}
{% if not role_perms %}
<span class="text-gray-500 text-xs">No permissions</span>
{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
<!-- Edit User Modal -->
<div id="edit-user-modal" class="hidden fixed inset-0 bg-black/30 backdrop-blur-md flex items-center justify-center z-50">
<div class="bg-gray-200 dark:bg-zinc-800 p-6 rounded-lg shadow-lg max-w-md w-full mx-4">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold">Edit User</h2>
<button onclick="closeEditUserModal()" class="text-gray-500 hover:text-gray-700 hover:cursor-pointer">
<i class="fas fa-times"></i>
</button>
</div>
<form action="/users" method="POST" class="space-y-4">
<input type="hidden" name="action" value="edit_user">
<input type="hidden" name="user_id" id="edit-user-id">
<input type="text" name="name" id="edit-user-name" placeholder="Name" class="border p-3 rounded-lg bg-gray-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 border-gray-600 w-full" required>
<input type="email" name="email" id="edit-user-email" placeholder="Email Address" class="border p-3 rounded-lg bg-gray-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 border-gray-600 w-full" required>
<input type="password" name="password" id="edit-user-password" placeholder="New Password (leave blank to keep)" class="border p-3 rounded-lg bg-gray-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 border-gray-600 w-full">
<select name="role_id" id="edit-user-role" class="border p-3 rounded-lg bg-gray-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 border-gray-600 w-full">
<option value="">No Role</option>
{% for role in roles %}
<option value="{{ role[0] }}">{{ role[1] }}</option>
{% endfor %}
</select>
<div class="flex justify-end space-x-2">
<button type="button" onclick="closeEditUserModal()" class="px-4 py-2 bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer rounded-lg">Cancel</button>
<button type="submit" class="px-4 py-2 bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer rounded-lg">Save</button>
</div>
</form>
</div>
</div>
<!-- Add Role Modal -->
<div id="add-role-modal" class="hidden fixed inset-0 bg-black/30 backdrop-blur-md flex items-center justify-center z-50 overflow-y-auto py-8">
<div class="bg-gray-200 dark:bg-zinc-800 p-6 rounded-lg shadow-lg max-w-3xl w-full mx-4 my-8">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold">Add New Role</h2>
<button onclick="closeAddRoleModal()" class="text-gray-500 hover:text-gray-700 hover:cursor-pointer">
<i class="fas fa-times"></i>
</button>
</div>
<form action="/users" method="POST" class="space-y-4">
<input type="hidden" name="action" value="add_role">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<input type="text" name="role_name" placeholder="Role Name" class="border p-3 rounded-lg bg-gray-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 border-gray-600" required>
<input type="text" name="role_description" placeholder="Description (optional)" class="border p-3 rounded-lg bg-gray-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 border-gray-600">
</div>
<div class="mb-4">
<h3 class="font-bold mb-3">Permissions</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 max-h-96 overflow-y-auto border border-gray-600 rounded-lg p-4 bg-gray-300 dark:bg-zinc-900">
<!-- View Permissions -->
<div class="col-span-full">
<h4 class="font-semibold text-base mb-2 border-b border-gray-500 pb-1">View Permissions</h4>
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
{% for perm in permissions %}
{% if perm[3] == 'View' %}
<label class="flex items-center mb-2 cursor-pointer hover:bg-gray-200 dark:hover:bg-zinc-800 p-2 rounded">
<input type="checkbox" name="permissions" value="{{ perm[0] }}" class="mr-2">
<span class="text-sm">{{ perm[2] }}</span>
</label>
{% endif %}
{% endfor %}
</div>
</div>
<!-- Device Management -->
<div>
<h4 class="font-semibold text-base mb-2 border-b border-gray-500 pb-1">Device Management</h4>
{% for perm in permissions %}
{% if perm[3] == 'Device' %}
<label class="flex items-center mb-2 cursor-pointer hover:bg-gray-200 dark:hover:bg-zinc-800 p-2 rounded">
<input type="checkbox" name="permissions" value="{{ perm[0] }}" class="mr-2">
<span class="text-sm">{{ perm[2] }}</span>
</label>
{% endif %}
{% endfor %}
{% for perm in permissions %}
{% if perm[3] == 'Device Type' %}
<label class="flex items-center mb-2 cursor-pointer hover:bg-gray-200 dark:hover:bg-zinc-800 p-2 rounded">
<input type="checkbox" name="permissions" value="{{ perm[0] }}" class="mr-2">
<span class="text-sm">{{ perm[2] }}</span>
</label>
{% endif %}
{% endfor %}
</div>
<!-- Network Management -->
<div>
<h4 class="font-semibold text-base mb-2 border-b border-gray-500 pb-1">Network Management</h4>
{% for perm in permissions %}
{% if perm[3] == 'Subnet' %}
<label class="flex items-center mb-2 cursor-pointer hover:bg-gray-200 dark:hover:bg-zinc-800 p-2 rounded">
<input type="checkbox" name="permissions" value="{{ perm[0] }}" class="mr-2">
<span class="text-sm">{{ perm[2] }}</span>
</label>
{% endif %}
{% endfor %}
{% for perm in permissions %}
{% if perm[3] == 'DHCP' %}
<label class="flex items-center mb-2 cursor-pointer hover:bg-gray-200 dark:hover:bg-zinc-800 p-2 rounded">
<input type="checkbox" name="permissions" value="{{ perm[0] }}" class="mr-2">
<span class="text-sm">{{ perm[2] }}</span>
</label>
{% endif %}
{% endfor %}
</div>
<!-- Rack Management -->
<div>
<h4 class="font-semibold text-base mb-2 border-b border-gray-500 pb-1">Rack Management</h4>
{% for perm in permissions %}
{% if perm[3] == 'Rack' %}
<label class="flex items-center mb-2 cursor-pointer hover:bg-gray-200 dark:hover:bg-zinc-800 p-2 rounded">
<input type="checkbox" name="permissions" value="{{ perm[0] }}" class="mr-2">
<span class="text-sm">{{ perm[2] }}</span>
</label>
{% endif %}
{% endfor %}
</div>
<!-- Admin -->
<div>
<h4 class="font-semibold text-base mb-2 border-b border-gray-500 pb-1">Administration</h4>
{% for perm in permissions %}
{% if perm[3] == 'Admin' %}
<label class="flex items-center mb-2 cursor-pointer hover:bg-gray-200 dark:hover:bg-zinc-800 p-2 rounded">
<input type="checkbox" name="permissions" value="{{ perm[0] }}" class="mr-2">
<span class="text-sm">{{ perm[2] }}</span>
</label>
{% endif %}
{% endfor %}
</div>
</div>
</div>
<div class="flex justify-end space-x-2">
<button type="button" onclick="closeAddRoleModal()" class="px-4 py-2 bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer rounded-lg">Cancel</button>
<button type="submit" class="px-4 py-2 bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer rounded-lg">Add Role</button>
</div>
</form>
</div>
</div>
<!-- Edit Role Modal -->
<div id="edit-role-modal" class="hidden fixed inset-0 bg-black/30 backdrop-blur-md flex items-center justify-center z-50 overflow-y-auto py-8">
<div class="bg-gray-200 dark:bg-zinc-800 p-6 rounded-lg shadow-lg max-w-3xl w-full mx-4 my-8">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold">Edit Role</h2>
<button onclick="closeEditRoleModal()" class="text-gray-500 hover:text-gray-700 hover:cursor-pointer">
<i class="fas fa-times"></i>
</button>
</div>
<form action="/users" method="POST" class="space-y-4">
<input type="hidden" name="action" value="edit_role">
<input type="hidden" name="role_id" id="edit-role-id">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<input type="text" name="role_name" id="edit-role-name" placeholder="Role Name" class="border p-3 rounded-lg bg-gray-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 border-gray-600" required>
<input type="text" name="role_description" id="edit-role-description" placeholder="Description (optional)" class="border p-3 rounded-lg bg-gray-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 border-gray-600">
</div>
<div class="mb-4">
<h3 class="font-bold mb-3">Permissions</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 max-h-96 overflow-y-auto border border-gray-600 rounded-lg p-4 bg-gray-300 dark:bg-zinc-900" id="edit-role-permissions">
<!-- Permissions will be populated by JavaScript -->
</div>
</div>
<div class="flex justify-end space-x-2">
<button type="button" onclick="closeEditRoleModal()" class="px-4 py-2 bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer rounded-lg">Cancel</button>
<button type="submit" class="px-4 py-2 bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer rounded-lg">Save</button>
</div>
</form>
</div>
</div>
<script>
const permissions = {{ permissions | tojson | safe }};
const rolePermissions = {{ role_permissions | tojson | safe }};
function showTab(tab) {
document.getElementById('users-tab').classList.add('hidden');
document.getElementById('roles-tab').classList.add('hidden');
document.getElementById('tab-users').classList.remove('border-blue-500', 'text-blue-600', 'dark:text-blue-400');
document.getElementById('tab-users').classList.add('border-transparent', 'text-gray-600', 'dark:text-gray-400');
document.getElementById('tab-roles').classList.remove('border-blue-500', 'text-blue-600', 'dark:text-blue-400');
document.getElementById('tab-roles').classList.add('border-transparent', 'text-gray-600', 'dark:text-gray-400');
if (tab === 'users') {
document.getElementById('users-tab').classList.remove('hidden');
document.getElementById('tab-users').classList.remove('border-transparent', 'text-gray-600', 'dark:text-gray-400');
document.getElementById('tab-users').classList.add('border-blue-500', 'text-blue-600', 'dark:text-blue-400');
} else {
document.getElementById('roles-tab').classList.remove('hidden');
document.getElementById('tab-roles').classList.remove('border-transparent', 'text-gray-600', 'dark:text-gray-400');
document.getElementById('tab-roles').classList.add('border-blue-500', 'text-blue-600', 'dark:text-blue-400');
}
}
function editUser(userId, name, email, roleId) {
document.getElementById('edit-user-id').value = userId;
document.getElementById('edit-user-name').value = name;
document.getElementById('edit-user-email').value = email;
document.getElementById('edit-user-password').value = '';
document.getElementById('edit-user-role').value = (roleId === null || roleId === 'null') ? '' : roleId;
document.getElementById('edit-user-modal').classList.remove('hidden');
}
function closeEditUserModal() {
document.getElementById('edit-user-modal').classList.add('hidden');
}
function showAddRoleModal() {
// Make sure edit modal is closed first
document.getElementById('edit-role-modal').classList.add('hidden');
// Clear any form data
const addForm = document.querySelector('#add-role-modal form');
if (addForm) {
addForm.reset();
}
// Show add modal
document.getElementById('add-role-modal').classList.remove('hidden');
}
function closeAddRoleModal() {
document.getElementById('add-role-modal').classList.add('hidden');
}
function editRole(roleId, roleName, roleDescription) {
// Make sure add modal is closed first
document.getElementById('add-role-modal').classList.add('hidden');
document.getElementById('edit-role-id').value = roleId;
document.getElementById('edit-role-name').value = roleName;
document.getElementById('edit-role-description').value = roleDescription || '';
const permissionsDiv = document.getElementById('edit-role-permissions');
permissionsDiv.innerHTML = '';
const rolePerms = rolePermissions[roleId] || [];
// Group permissions by merged categories
const viewPerms = permissions.filter(p => p[3] === 'View');
const devicePerms = permissions.filter(p => p[3] === 'Device');
const deviceTypePerms = permissions.filter(p => p[3] === 'Device Type');
const subnetPerms = permissions.filter(p => p[3] === 'Subnet');
const dhcpPerms = permissions.filter(p => p[3] === 'DHCP');
const rackPerms = permissions.filter(p => p[3] === 'Rack');
const adminPerms = permissions.filter(p => p[3] === 'Admin');
let html = '';
// View Permissions
html += ' <!-- View Permissions -->\n';
html += ' <div class="col-span-full">\n';
html += ' <h4 class="font-semibold text-base mb-2 border-b border-gray-500 pb-1">View Permissions</h4>\n';
html += ' <div class="grid grid-cols-1 md:grid-cols-2 gap-2">\n';
viewPerms.forEach(perm => {
const checked = rolePerms.includes(perm[0]) ? 'checked' : '';
html += ` <label class="flex items-center mb-2 cursor-pointer hover:bg-gray-200 dark:hover:bg-zinc-800 p-2 rounded">
<input type="checkbox" name="permissions" value="${perm[0]}" ${checked} class="mr-2">
<span class="text-sm">${perm[2]}</span>
</label>\n`;
});
html += ' </div>\n';
html += ' </div>\n';
html += ' \n';
// Device Management
html += ' <!-- Device Management -->\n';
html += ' <div>\n';
html += ' <h4 class="font-semibold text-base mb-2 border-b border-gray-500 pb-1">Device Management</h4>\n';
devicePerms.forEach(perm => {
const checked = rolePerms.includes(perm[0]) ? 'checked' : '';
html += ` <label class="flex items-center mb-2 cursor-pointer hover:bg-gray-200 dark:hover:bg-zinc-800 p-2 rounded">
<input type="checkbox" name="permissions" value="${perm[0]}" ${checked} class="mr-2">
<span class="text-sm">${perm[2]}</span>
</label>\n`;
});
deviceTypePerms.forEach(perm => {
const checked = rolePerms.includes(perm[0]) ? 'checked' : '';
html += ` <label class="flex items-center mb-2 cursor-pointer hover:bg-gray-200 dark:hover:bg-zinc-800 p-2 rounded">
<input type="checkbox" name="permissions" value="${perm[0]}" ${checked} class="mr-2">
<span class="text-sm">${perm[2]}</span>
</label>\n`;
});
html += ' </div>\n';
html += ' \n';
// Network Management
html += ' <!-- Network Management -->\n';
html += ' <div>\n';
html += ' <h4 class="font-semibold text-base mb-2 border-b border-gray-500 pb-1">Network Management</h4>\n';
subnetPerms.forEach(perm => {
const checked = rolePerms.includes(perm[0]) ? 'checked' : '';
html += ` <label class="flex items-center mb-2 cursor-pointer hover:bg-gray-200 dark:hover:bg-zinc-800 p-2 rounded">
<input type="checkbox" name="permissions" value="${perm[0]}" ${checked} class="mr-2">
<span class="text-sm">${perm[2]}</span>
</label>\n`;
});
dhcpPerms.forEach(perm => {
const checked = rolePerms.includes(perm[0]) ? 'checked' : '';
html += ` <label class="flex items-center mb-2 cursor-pointer hover:bg-gray-200 dark:hover:bg-zinc-800 p-2 rounded">
<input type="checkbox" name="permissions" value="${perm[0]}" ${checked} class="mr-2">
<span class="text-sm">${perm[2]}</span>
</label>\n`;
});
html += ' </div>\n';
html += ' \n';
// Rack Management
html += ' <!-- Rack Management -->\n';
html += ' <div>\n';
html += ' <h4 class="font-semibold text-base mb-2 border-b border-gray-500 pb-1">Rack Management</h4>\n';
rackPerms.forEach(perm => {
const checked = rolePerms.includes(perm[0]) ? 'checked' : '';
html += ` <label class="flex items-center mb-2 cursor-pointer hover:bg-gray-200 dark:hover:bg-zinc-800 p-2 rounded">
<input type="checkbox" name="permissions" value="${perm[0]}" ${checked} class="mr-2">
<span class="text-sm">${perm[2]}</span>
</label>\n`;
});
html += ' </div>\n';
html += ' \n';
// Admin
html += ' <!-- Admin -->\n';
html += ' <div>\n';
html += ' <h4 class="font-semibold text-base mb-2 border-b border-gray-500 pb-1">Administration</h4>\n';
adminPerms.forEach(perm => {
const checked = rolePerms.includes(perm[0]) ? 'checked' : '';
html += ` <label class="flex items-center mb-2 cursor-pointer hover:bg-gray-200 dark:hover:bg-zinc-800 p-2 rounded">
<input type="checkbox" name="permissions" value="${perm[0]}" ${checked} class="mr-2">
<span class="text-sm">${perm[2]}</span>
</label>\n`;
});
html += ' </div>\n';
permissionsDiv.innerHTML = html;
document.getElementById('edit-role-modal').classList.remove('hidden');
}
function closeEditRoleModal() {
document.getElementById('edit-role-modal').classList.add('hidden');
}
function deleteRole(roleId, roleName) {
if (confirm(`Are you sure you want to delete the role "${roleName}"?`)) {
const form = document.createElement('form');
form.method = 'POST';
form.action = '/users';
form.innerHTML = `
<input type="hidden" name="action" value="delete_role">
<input type="hidden" name="role_id" value="${roleId}">
`;
document.body.appendChild(form);
form.submit();
}
}
// Close modals when clicking outside
window.onclick = function(event) {
const editUserModal = document.getElementById('edit-user-modal');
const editRoleModal = document.getElementById('edit-role-modal');
const addRoleModal = document.getElementById('add-role-modal');
if (event.target === editUserModal) {
closeEditUserModal();
}
if (event.target === editRoleModal) {
closeEditRoleModal();
}
if (event.target === addRoleModal) {
closeAddRoleModal();
}
}
</script>
</body>
</html>