Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8909834a19 | |||
| 59662ec4d8 | |||
| e0b6c22c1f | |||
| c53472c5d7 | |||
| fdd8b36fbf | |||
| 0efa310d50 | |||
| 3bf2697010 |
@@ -1,2 +1,51 @@
|
|||||||
|
# Documentation
|
||||||
|
README.md
|
||||||
|
CHANGELOG.md
|
||||||
|
*.md
|
||||||
|
|
||||||
|
# Deployment files
|
||||||
deployment.yml
|
deployment.yml
|
||||||
run.sh
|
run.sh
|
||||||
|
Dockerfile
|
||||||
|
.dockerignore
|
||||||
|
|
||||||
|
# Git
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.gitattributes
|
||||||
|
|
||||||
|
# Python cache
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
ENV/
|
||||||
|
.venv
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Environment files
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
logs/
|
||||||
|
|
||||||
|
# Build tools
|
||||||
|
tailwindcss
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
".": "1.2.0"
|
".": "1.4.0"
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,19 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [1.4.0](https://github.com/JDB-NET/ipam/compare/v1.3.0...v1.4.0) (2025-11-06)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* :sparkles: full api integration ([c53472c](https://github.com/JDB-NET/ipam/commit/c53472c5d760e28e53a737cb0546e85c9a422d15))
|
||||||
|
|
||||||
|
## [1.3.0](https://github.com/JDB-NET/ipam/compare/v1.2.0...v1.3.0) (2025-11-06)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* :sparkles: role based access control ([3bf2697](https://github.com/JDB-NET/ipam/commit/3bf269701030bc1f14a48c5af488286c424dbfa7))
|
||||||
|
|
||||||
## [1.2.0](https://github.com/JDB-NET/ipam/compare/v1.1.1...v1.2.0) (2025-11-06)
|
## [1.2.0](https://github.com/JDB-NET/ipam/compare/v1.1.1...v1.2.0) (2025-11-06)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ A Flask-based web application for comprehensive IP Address Management (IPAM). Ma
|
|||||||
- **Site Organization**: Organize subnets and devices by site/location
|
- **Site Organization**: Organize subnets and devices by site/location
|
||||||
- **Audit Logging**: Complete audit trail of all changes with user, action, details, and timestamps
|
- **Audit Logging**: Complete audit trail of all changes with user, action, details, and timestamps
|
||||||
- **User Management**: Multi-user support with secure password authentication
|
- **User Management**: Multi-user support with secure password authentication
|
||||||
|
- **Role-Based Access Control (RBAC)**: Granular permission system with default roles (admin, user, view_only) and custom role creation
|
||||||
|
- **REST API**: Full-featured REST API with API key authentication for programmatic access
|
||||||
- **CSV Export**: Export subnet and rack data to CSV files
|
- **CSV Export**: Export subnet and rack data to CSV files
|
||||||
- **Device Statistics**: View device counts by type
|
- **Device Statistics**: View device counts by type
|
||||||
- **Web Interface**: Modern, responsive web GUI built with Tailwind CSS and dark mode support
|
- **Web Interface**: Modern, responsive web GUI built with Tailwind CSS and dark mode support
|
||||||
@@ -142,6 +144,61 @@ View all changes and actions in the "Audit Log" section, with filtering by user,
|
|||||||
- **Subnet CSV**: Click "Export CSV" on any subnet page to download IP addresses with hostnames
|
- **Subnet CSV**: Click "Export CSV" on any subnet page to download IP addresses with hostnames
|
||||||
- **Rack CSV**: Click "Export CSV" on any rack page to download rack layout information
|
- **Rack CSV**: Click "Export CSV" on any rack page to download rack layout information
|
||||||
|
|
||||||
|
### Role-Based Access Control
|
||||||
|
|
||||||
|
The system uses a granular role-based access control (RBAC) system to manage user permissions:
|
||||||
|
|
||||||
|
1. **Default Roles**:
|
||||||
|
- **Admin**: Full access to all features including user and role management
|
||||||
|
- **User**: Can view and manage most features (devices, subnets, racks, etc.) but cannot manage users or roles
|
||||||
|
- **View Only**: Read-only access to view pages but cannot make any changes
|
||||||
|
|
||||||
|
2. **Custom Roles**: Administrators can create custom roles with specific permission sets from the Users page
|
||||||
|
|
||||||
|
3. **Permission Granularity**: Permissions are organized into categories:
|
||||||
|
- View permissions (access to pages)
|
||||||
|
- Device Management (add, edit, delete devices)
|
||||||
|
- Network Management (subnet operations)
|
||||||
|
- Rack Management (rack operations)
|
||||||
|
- DHCP Configuration
|
||||||
|
- Administration (user and role management)
|
||||||
|
|
||||||
|
4. **User Management**: Navigate to the Users page to:
|
||||||
|
- Create and manage users
|
||||||
|
- Assign roles to users
|
||||||
|
- Create custom roles with specific permissions
|
||||||
|
- View and regenerate API keys
|
||||||
|
|
||||||
|
### REST API
|
||||||
|
|
||||||
|
The application includes a comprehensive REST API for programmatic access:
|
||||||
|
|
||||||
|
1. **Authentication**: All API requests require an API key, which can be provided via:
|
||||||
|
- `X-API-Key` header
|
||||||
|
- `Authorization: Bearer <api_key>` header
|
||||||
|
- `?api_key=<api_key>` query parameter
|
||||||
|
|
||||||
|
2. **Base URL**: All API endpoints are prefixed with `/api/v1`
|
||||||
|
|
||||||
|
3. **Available Endpoints**:
|
||||||
|
- **Devices**: `GET`, `POST`, `PUT`, `DELETE /api/v1/devices`
|
||||||
|
- **Subnets**: `GET`, `POST`, `PUT`, `DELETE /api/v1/subnets`
|
||||||
|
- **Racks**: `GET`, `POST`, `DELETE /api/v1/racks`
|
||||||
|
- **Device Types**: `GET /api/v1/device-types`
|
||||||
|
- **DHCP**: `GET`, `POST /api/v1/subnets/{id}/dhcp`
|
||||||
|
- **Audit Log**: `GET /api/v1/audit`
|
||||||
|
- **Users & Roles**: `GET /api/v1/users`, `GET /api/v1/roles` (admin only)
|
||||||
|
|
||||||
|
4. **API Keys**: Each user has a unique API key that can be viewed and regenerated from the Users page. API keys respect the same role-based permissions as the web interface.
|
||||||
|
|
||||||
|
5. **Documentation**: Full API documentation is available in the Help page of the web interface.
|
||||||
|
|
||||||
|
**Example API Request**:
|
||||||
|
```bash
|
||||||
|
curl -H "X-API-Key: your_api_key" \
|
||||||
|
https://your-server:5000/api/v1/devices
|
||||||
|
```
|
||||||
|
|
||||||
## Kubernetes Deployment
|
## Kubernetes Deployment
|
||||||
|
|
||||||
The project includes a Kubernetes deployment manifest. See `deployment.yml` for details.
|
The project includes a Kubernetes deployment manifest. See `deployment.yml` for details.
|
||||||
@@ -190,6 +247,9 @@ spec:
|
|||||||
- Ensure database connections are secured (consider SSL/TLS for MySQL connections)
|
- Ensure database connections are secured (consider SSL/TLS for MySQL connections)
|
||||||
- Review audit logs regularly for unauthorized changes
|
- Review audit logs regularly for unauthorized changes
|
||||||
- Limit database user permissions if possible (though CREATE/ALTER may be needed for schema initialization)
|
- Limit database user permissions if possible (though CREATE/ALTER may be needed for schema initialization)
|
||||||
|
- **API Keys**: Keep API keys secure and never share them publicly. Regenerate keys if they may have been compromised
|
||||||
|
- **Role-Based Access**: Use the RBAC system to grant users only the permissions they need (principle of least privilege)
|
||||||
|
- **HTTPS**: Use HTTPS in production to protect API keys and session data in transit
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from flask import Flask
|
from flask import Flask, session
|
||||||
from db import init_db, hash_password
|
from db import init_db, hash_password, get_db_connection
|
||||||
from routes import register_routes
|
from routes import register_routes
|
||||||
import os
|
import os
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
@@ -26,10 +26,14 @@ def inject_env_vars():
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# Import has_permission from routes after routes are registered
|
||||||
|
from routes import has_permission
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'NAME': os.environ.get('NAME', 'JDB-NET'),
|
'NAME': os.environ.get('NAME', 'JDB-NET'),
|
||||||
'LOGO_PNG': os.environ.get('LOGO_PNG', 'https://assets.s3.jdbnet.co.uk/logo/128x128.png'),
|
'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)
|
register_routes(app)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
import hashlib
|
import hashlib
|
||||||
import base64
|
import base64
|
||||||
|
import secrets
|
||||||
import mysql.connector
|
import mysql.connector
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
|
|
||||||
@@ -18,6 +19,10 @@ def verify_password(password, hashed):
|
|||||||
return False
|
return False
|
||||||
return hash_password(password, salt) == hashed
|
return hash_password(password, salt) == hashed
|
||||||
|
|
||||||
|
def generate_api_key():
|
||||||
|
"""Generate a secure API key"""
|
||||||
|
return secrets.token_urlsafe(32)
|
||||||
|
|
||||||
def get_db_connection(app=None):
|
def get_db_connection(app=None):
|
||||||
if app is None:
|
if app is None:
|
||||||
app = current_app
|
app = current_app
|
||||||
@@ -149,9 +154,206 @@ def init_db(app=None):
|
|||||||
except mysql.connector.Error as e:
|
except mysql.connector.Error as e:
|
||||||
if e.errno != 1061 and e.errno != 1826 and 'Duplicate' not in str(e):
|
if e.errno != 1061 and e.errno != 1826 and 'Duplicate' not in str(e):
|
||||||
raise
|
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
|
||||||
|
|
||||||
|
# Add api_key column to User table if it doesn't exist
|
||||||
|
cursor.execute("SHOW COLUMNS FROM User LIKE 'api_key'")
|
||||||
|
if not cursor.fetchone():
|
||||||
|
cursor.execute('ALTER TABLE User ADD COLUMN api_key VARCHAR(255) DEFAULT NULL UNIQUE')
|
||||||
|
|
||||||
|
# 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,))
|
||||||
|
|
||||||
|
# Generate API keys for users that don't have one
|
||||||
|
cursor.execute('SELECT id FROM User WHERE api_key IS NULL')
|
||||||
|
users_without_api_key = cursor.fetchall()
|
||||||
|
for (user_id,) in users_without_api_key:
|
||||||
|
api_key = generate_api_key()
|
||||||
|
cursor.execute('UPDATE User SET api_key = %s WHERE id = %s', (api_key, user_id))
|
||||||
|
|
||||||
cursor.execute('SELECT COUNT(*) FROM User')
|
cursor.execute('SELECT COUNT(*) FROM User')
|
||||||
if cursor.fetchone()[0] == 0:
|
if cursor.fetchone()[0] == 0:
|
||||||
cursor.execute('''INSERT INTO User (name, email, password) VALUES (%s, %s, %s)''',
|
api_key = generate_api_key()
|
||||||
('admin', 'admin@example.com', hash_password('password')))
|
cursor.execute('''INSERT INTO User (name, email, password, role_id, api_key) VALUES (%s, %s, %s, %s, %s)''',
|
||||||
|
('admin', 'admin@example.com', hash_password('password'), admin_role_id, api_key))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|||||||
+206
-30
@@ -10,44 +10,220 @@
|
|||||||
</head>
|
</head>
|
||||||
<body class="bg-gray-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 min-h-screen flex flex-col">
|
<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' %}
|
{% include 'header.html' %}
|
||||||
<div class="flex-1 flex items-center justify-center mx-4">
|
<div class="flex-1 mx-4 py-8 pt-20">
|
||||||
<div class="container py-8 max-w-md pt-20">
|
<div class="container max-w-7xl mx-auto">
|
||||||
<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 Panel</h1>
|
||||||
<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>
|
|
||||||
{% if error %}
|
{% 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">
|
||||||
{% endif %}
|
{{ error }}
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
{% endif %}
|
||||||
<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>
|
<!-- Quick Links -->
|
||||||
<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.');">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-8">
|
||||||
<select name="subnet_id" class="border p-3 rounded-lg bg-gray-300 dark:bg-zinc-900 border-gray-600" required>
|
<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">
|
||||||
<option value="" disabled selected>Select Subnet</option>
|
<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>
|
||||||
|
|
||||||
|
{% 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 %}
|
{% for subnet in subnets %}
|
||||||
<option value="{{ subnet.id }}">{{ subnet.name }} ({{ subnet.cidr }})</option>
|
<tr class="border-b border-gray-600 hover:bg-gray-300 dark:hover:bg-zinc-700">
|
||||||
{% endfor %}
|
<td class="p-3 font-medium">{{ subnet.name }}</td>
|
||||||
</select>
|
<td class="p-3 font-mono text-sm">{{ subnet.cidr }}</td>
|
||||||
<button type="submit" class="text-red-500 hover:text-red-700 hover:cursor-pointer rounded-full p-3" title="Delete Subnet">
|
<td class="p-3">
|
||||||
<i class="fas fa-trash fa-lg"></i>
|
<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>
|
</button>
|
||||||
</form>
|
</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>
|
</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 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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -7,12 +7,27 @@
|
|||||||
{% if current_user_name %}Hello, {{ current_user_name }}{% endif %}
|
{% if current_user_name %}Hello, {{ current_user_name }}{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<nav class="hidden md:flex items-center space-x-6" id="main-nav">
|
<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>
|
<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>
|
<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>
|
<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>
|
<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>
|
<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>
|
<a href="/help" class="text-gray-200 hover:text-gray-400 font-medium">Help</a>
|
||||||
|
{% endif %}
|
||||||
{% if current_user_name %}
|
{% if current_user_name %}
|
||||||
<a href="/logout" class="text-gray-200 hover:text-gray-400 font-medium">Logout</a>
|
<a href="/logout" class="text-gray-200 hover:text-gray-400 font-medium">Logout</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -23,12 +38,27 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</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">
|
<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>
|
<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>
|
<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>
|
<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>
|
<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>
|
<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>
|
<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 %}
|
{% if current_user_name %}
|
||||||
<a href="/logout" class="block px-6 py-2 text-gray-200 hover:text-gray-400 font-medium">Logout</a>
|
<a href="/logout" class="block px-6 py-2 text-gray-200 hover:text-gray-400 font-medium">Logout</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
+101
-4
@@ -19,7 +19,7 @@
|
|||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-xl font-semibold mb-1">Managing Subnets</h3>
|
<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>
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-xl font-semibold mb-1">Adding a Device</h3>
|
<h3 class="text-xl font-semibold mb-1">Adding a Device</h3>
|
||||||
@@ -56,12 +56,109 @@
|
|||||||
<h2 class="text-2xl font-semibold mb-4 border-b border-gray-700 pb-2">User Management & Audit</h2>
|
<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 class="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-xl font-semibold mb-1">User Management & Admin</h3>
|
<h3 class="text-xl font-semibold mb-1">User & Role Management</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>
|
<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>
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-xl font-semibold mb-1">Audit & History</h3>
|
<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>
|
||||||
|
<h3 class="text-xl font-semibold mb-1">API Keys</h3>
|
||||||
|
<p>Each user has a unique API key that can be used to authenticate API requests. API keys can be viewed and regenerated from the <a href="/users" class="text-blue-600 dark:text-blue-400 hover:underline">Users</a> page. Keep your API key secure and never share it publicly.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-gray-200 dark:bg-zinc-800 rounded-2xl shadow-lg p-8">
|
||||||
|
<h2 class="text-2xl font-semibold mb-4 border-b border-gray-700 pb-2">API Documentation</h2>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-xl font-semibold mb-1">Authentication</h3>
|
||||||
|
<p>All API requests require authentication using an API key. You can provide the API key in one of three ways:</p>
|
||||||
|
<ul class="list-disc list-inside mt-2 space-y-1 ml-4">
|
||||||
|
<li><strong>Header:</strong> <code class="bg-gray-300 dark:bg-zinc-700 px-2 py-1 rounded text-sm">X-API-Key: your_api_key</code></li>
|
||||||
|
<li><strong>Authorization Header:</strong> <code class="bg-gray-300 dark:bg-zinc-700 px-2 py-1 rounded text-sm">Authorization: Bearer your_api_key</code></li>
|
||||||
|
<li><strong>Query Parameter:</strong> <code class="bg-gray-300 dark:bg-zinc-700 px-2 py-1 rounded text-sm">?api_key=your_api_key</code></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-xl font-semibold mb-1">Base URL</h3>
|
||||||
|
<p>All API endpoints are prefixed with <code class="bg-gray-300 dark:bg-zinc-700 px-2 py-1 rounded text-sm">/api/v1</code></p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-xl font-semibold mb-1">Available Endpoints</h3>
|
||||||
|
<div class="space-y-3 mt-2">
|
||||||
|
<div>
|
||||||
|
<h4 class="font-semibold">Devices</h4>
|
||||||
|
<ul class="list-disc list-inside ml-4 text-sm space-y-1">
|
||||||
|
<li><code class="bg-gray-300 dark:bg-zinc-700 px-1 rounded">GET /api/v1/devices</code> - List all devices</li>
|
||||||
|
<li><code class="bg-gray-300 dark:bg-zinc-700 px-1 rounded">GET /api/v1/devices/{id}</code> - Get device details</li>
|
||||||
|
<li><code class="bg-gray-300 dark:bg-zinc-700 px-1 rounded">POST /api/v1/devices</code> - Create device</li>
|
||||||
|
<li><code class="bg-gray-300 dark:bg-zinc-700 px-1 rounded">PUT /api/v1/devices/{id}</code> - Update device</li>
|
||||||
|
<li><code class="bg-gray-300 dark:bg-zinc-700 px-1 rounded">DELETE /api/v1/devices/{id}</code> - Delete device</li>
|
||||||
|
<li><code class="bg-gray-300 dark:bg-zinc-700 px-1 rounded">POST /api/v1/devices/{id}/ips</code> - Add IP to device</li>
|
||||||
|
<li><code class="bg-gray-300 dark:bg-zinc-700 px-1 rounded">DELETE /api/v1/devices/{id}/ips/{ip_id}</code> - Remove IP from device</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 class="font-semibold">Subnets</h4>
|
||||||
|
<ul class="list-disc list-inside ml-4 text-sm space-y-1">
|
||||||
|
<li><code class="bg-gray-300 dark:bg-zinc-700 px-1 rounded">GET /api/v1/subnets</code> - List all subnets</li>
|
||||||
|
<li><code class="bg-gray-300 dark:bg-zinc-700 px-1 rounded">GET /api/v1/subnets/{id}</code> - Get subnet details</li>
|
||||||
|
<li><code class="bg-gray-300 dark:bg-zinc-700 px-1 rounded">POST /api/v1/subnets</code> - Create subnet</li>
|
||||||
|
<li><code class="bg-gray-300 dark:bg-zinc-700 px-1 rounded">PUT /api/v1/subnets/{id}</code> - Update subnet</li>
|
||||||
|
<li><code class="bg-gray-300 dark:bg-zinc-700 px-1 rounded">DELETE /api/v1/subnets/{id}</code> - Delete subnet</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 class="font-semibold">Racks</h4>
|
||||||
|
<ul class="list-disc list-inside ml-4 text-sm space-y-1">
|
||||||
|
<li><code class="bg-gray-300 dark:bg-zinc-700 px-1 rounded">GET /api/v1/racks</code> - List all racks</li>
|
||||||
|
<li><code class="bg-gray-300 dark:bg-zinc-700 px-1 rounded">GET /api/v1/racks/{id}</code> - Get rack details</li>
|
||||||
|
<li><code class="bg-gray-300 dark:bg-zinc-700 px-1 rounded">POST /api/v1/racks</code> - Create rack</li>
|
||||||
|
<li><code class="bg-gray-300 dark:bg-zinc-700 px-1 rounded">DELETE /api/v1/racks/{id}</code> - Delete rack</li>
|
||||||
|
<li><code class="bg-gray-300 dark:bg-zinc-700 px-1 rounded">POST /api/v1/racks/{id}/devices</code> - Add device to rack</li>
|
||||||
|
<li><code class="bg-gray-300 dark:bg-zinc-700 px-1 rounded">DELETE /api/v1/racks/{id}/devices/{rack_device_id}</code> - Remove device from rack</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 class="font-semibold">Other</h4>
|
||||||
|
<ul class="list-disc list-inside ml-4 text-sm space-y-1">
|
||||||
|
<li><code class="bg-gray-300 dark:bg-zinc-700 px-1 rounded">GET /api/v1/info</code> - Get API info and user details</li>
|
||||||
|
<li><code class="bg-gray-300 dark:bg-zinc-700 px-1 rounded">GET /api/v1/device-types</code> - List device types</li>
|
||||||
|
<li><code class="bg-gray-300 dark:bg-zinc-700 px-1 rounded">GET /api/v1/subnets/{id}/dhcp</code> - Get DHCP pools</li>
|
||||||
|
<li><code class="bg-gray-300 dark:bg-zinc-700 px-1 rounded">POST /api/v1/subnets/{id}/dhcp</code> - Configure DHCP pools</li>
|
||||||
|
<li><code class="bg-gray-300 dark:bg-zinc-700 px-1 rounded">GET /api/v1/audit</code> - Get audit log</li>
|
||||||
|
<li><code class="bg-gray-300 dark:bg-zinc-700 px-1 rounded">GET /api/v1/users</code> - List users (admin only)</li>
|
||||||
|
<li><code class="bg-gray-300 dark:bg-zinc-700 px-1 rounded">GET /api/v1/roles</code> - List roles (admin only)</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-xl font-semibold mb-1">Permissions</h3>
|
||||||
|
<p>API endpoints respect the same role-based permissions as the web interface. Users can only perform actions that their role allows. If a user lacks the required permission, the API will return a <code class="bg-gray-300 dark:bg-zinc-700 px-2 py-1 rounded text-sm">403 Forbidden</code> error.</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-xl font-semibold mb-1">Response Format</h3>
|
||||||
|
<p>All API responses are in JSON format. Successful requests return <code class="bg-gray-300 dark:bg-zinc-700 px-2 py-1 rounded text-sm">200 OK</code> or <code class="bg-gray-300 dark:bg-zinc-700 px-2 py-1 rounded text-sm">201 Created</code> with the requested data. Errors return appropriate HTTP status codes with an error message in the response body.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+523
-31
@@ -3,46 +3,538 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<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 rel="icon" type="image/png" href="{{ LOGO_PNG }}">
|
||||||
<link href="/static/css/output.css" rel="stylesheet">
|
<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">
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css" rel="stylesheet">
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-gray-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 min-h-screen flex flex-col">
|
<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' %}
|
{% include 'header.html' %}
|
||||||
<div class="flex-1 flex items-center justify-center mx-4">
|
<div class="flex-1 mx-4 py-8 pt-20">
|
||||||
<div class="container py-8 max-w-6xl pt-20">
|
<div class="container max-w-7xl mx-auto">
|
||||||
<h1 class="text-3xl font-bold mb-6 text-center">User Management</h1>
|
<h1 class="text-3xl font-bold mb-6 text-center">User & Role 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">
|
{% if error %}
|
||||||
<div class="flex flex-col space-y-4 items-center w-full">
|
<div class="bg-red-200 dark:bg-red-800 text-red-800 dark:text-red-200 p-4 rounded-lg mb-6">
|
||||||
<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>
|
{{ error }}
|
||||||
<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>
|
</div>
|
||||||
</form>
|
{% endif %}
|
||||||
<h2 class="text-xl font-bold mb-4">Existing Users</h2>
|
|
||||||
<ul class="space-y-4">
|
<!-- Tabs -->
|
||||||
{% for user in users %}
|
<div class="mb-6 border-b border-gray-600">
|
||||||
<li class="bg-gray-200 dark:bg-zinc-800 p-4 rounded-lg flex justify-between items-center">
|
<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">
|
||||||
<form action="/users" method="POST" class="flex flex-row items-center space-x-2">
|
Users
|
||||||
<input type="hidden" name="action" value="edit">
|
</button>
|
||||||
<input type="hidden" name="user_id" value="{{ user[0] }}">
|
<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">
|
||||||
<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">
|
Roles & Permissions
|
||||||
<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">
|
</button>
|
||||||
<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">
|
</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-3 py-1 rounded-lg">Save</button>
|
|
||||||
</form>
|
<!-- Users Tab -->
|
||||||
<form action="/users" method="POST" onsubmit="return confirm('Are you sure you want to delete this user?');">
|
<div id="users-tab" class="tab-content">
|
||||||
<input type="hidden" name="action" value="delete">
|
{% if can_manage_users %}
|
||||||
<input type="hidden" name="user_id" value="{{ user[0] }}">
|
<div class="bg-gray-200 dark:bg-zinc-800 p-6 rounded-lg shadow-md mb-8">
|
||||||
<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>
|
<h2 class="text-xl font-bold mb-4">Add New User</h2>
|
||||||
</form>
|
<form action="/users" method="POST" class="space-y-4">
|
||||||
</li>
|
<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 %}
|
{% endfor %}
|
||||||
</ul>
|
</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>
|
||||||
|
{% 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-center p-2">Role</th>
|
||||||
|
{% if can_manage_users %}
|
||||||
|
<th class="text-center 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 text-center">
|
||||||
|
<span class="px-2 py-1 bg-gray-300 dark:bg-zinc-700 rounded">{{ user[4] or 'No Role' }}</span>
|
||||||
|
</td>
|
||||||
|
{% if can_manage_users %}
|
||||||
|
<td class="p-2 text-center">
|
||||||
|
<button onclick="editUser({{ user[0] }}, '{{ user[1]|replace("'", "\\'") }}', '{{ user[2]|replace("'", "\\'") }}', {{ user[3] if user[3] else 'null' }}, '{{ user[5]|replace("'", "\\'") if user[5] else '' }}')" class="text-blue-500 hover:text-blue-700 mr-2 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 regenerate the API key for this user?');" class="inline mr-2">
|
||||||
|
<input type="hidden" name="action" value="regenerate_api_key">
|
||||||
|
<input type="hidden" name="user_id" value="{{ user[0] }}">
|
||||||
|
<button type="submit" class="text-yellow-500 hover:text-yellow-700 hover:cursor-pointer" title="Regenerate API Key">
|
||||||
|
<i class="fas fa-key"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<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>
|
</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="border p-3 rounded-lg bg-gray-300 dark:bg-zinc-900 border-gray-600 w-full">
|
||||||
|
<label class="text-sm font-semibold mb-2 block">API Key</label>
|
||||||
|
<code id="edit-user-api-key" class="text-xs font-mono break-all block bg-gray-200 dark:bg-zinc-800 px-2 py-1 rounded"></code>
|
||||||
|
<p class="text-xs text-gray-600 dark:text-gray-400 mt-2">Use this API key to authenticate API requests. Keep it secure!</p>
|
||||||
|
</div>
|
||||||
|
<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, apiKey) {
|
||||||
|
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-api-key').textContent = apiKey || 'No API Key';
|
||||||
|
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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user