Initial Commit
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
FROM mcr.microsoft.com/devcontainers/python:3.13
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
sqlite3 \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Set the working directory
|
||||
WORKDIR /workspace
|
||||
|
||||
# Default command
|
||||
CMD ["sleep", "infinity"]
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "Flask Dev Container",
|
||||
"build": {
|
||||
"dockerfile": "Dockerfile"
|
||||
},
|
||||
"settings": {},
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": ["ms-python.python"]
|
||||
}
|
||||
},
|
||||
"postCreateCommand": "pip install -r requirements.txt; curl -sLO https://github.com/tailwindlabs/tailwindcss/releases/latest/download/tailwindcss-linux-x64; chmod +x tailwindcss-linux-x64; mv tailwindcss-linux-x64 tailwindcss",
|
||||
"forwardPorts": [5000],
|
||||
"remoteUser": "vscode"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
db/
|
||||
__pycache__
|
||||
tailwindcss
|
||||
static/css/output.css
|
||||
@@ -0,0 +1,45 @@
|
||||
stages:
|
||||
- buildandpush
|
||||
- sonarqube_and_deploy
|
||||
|
||||
variables:
|
||||
IMAGE_NAME: "$HARBOR_REGISTRY/internal/ipam"
|
||||
|
||||
before_script:
|
||||
- echo "$HARBOR_PASSWORD" | docker login $HARBOR_REGISTRY -u "$HARBOR_USERNAME" --password-stdin
|
||||
|
||||
buildandpush:
|
||||
stage: buildandpush
|
||||
image: docker:latest
|
||||
tags:
|
||||
- docker
|
||||
script:
|
||||
- docker build -t $IMAGE_NAME:$IMAGE_TAG .
|
||||
- docker tag $IMAGE_NAME:$IMAGE_TAG $IMAGE_NAME:latest
|
||||
- docker push $IMAGE_NAME:$IMAGE_TAG
|
||||
- docker push $IMAGE_NAME:latest
|
||||
|
||||
sonarqube:
|
||||
stage: sonarqube_and_deploy
|
||||
image:
|
||||
name: sonarsource/sonar-scanner-cli:11
|
||||
entrypoint: [""]
|
||||
tags:
|
||||
- docker
|
||||
cache:
|
||||
policy: pull-push
|
||||
key: "sonar-cache-$CI_COMMIT_REF_SLUG"
|
||||
paths:
|
||||
- "${SONAR_USER_HOME}/cache"
|
||||
before_script: []
|
||||
script:
|
||||
- sonar-scanner -Dsonar.host.url="${SONAR_HOST_URL}"
|
||||
allow_failure: true
|
||||
|
||||
deploy:
|
||||
stage: sonarqube_and_deploy
|
||||
tags:
|
||||
- k3s-lan-01
|
||||
before_script: []
|
||||
script:
|
||||
- sudo kubectl replace -f deployment.yml --grace-period=60 --force
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
FROM python:3.13-slim
|
||||
WORKDIR /app
|
||||
COPY . /app
|
||||
RUN pip install -r requirements.txt
|
||||
RUN apt-get update && apt-get install -y sqlite3 curl
|
||||
RUN rm -rf /var/lib/apt/lists/*
|
||||
RUN curl -sLO https://github.com/tailwindlabs/tailwindcss/releases/latest/download/tailwindcss-linux-x64 \
|
||||
&& chmod +x tailwindcss-linux-x64 \
|
||||
&& mv tailwindcss-linux-x64 tailwindcss \
|
||||
&& ./tailwindcss -i ./static/css/input.css -o ./static/css/output.css --content "./templates/*.html,./static/js/*.js" --minify
|
||||
EXPOSE 5000
|
||||
CMD ["python", "app.py"]
|
||||
@@ -0,0 +1,15 @@
|
||||
from flask import Flask
|
||||
from db import init_db, hash_password
|
||||
from routes import register_routes
|
||||
import os
|
||||
|
||||
os.chdir(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config['SECRET_KEY'] = '#{flask-secret}#'
|
||||
|
||||
register_routes(app)
|
||||
|
||||
if __name__ == '__main__':
|
||||
init_db()
|
||||
app.run(host='0.0.0.0', port=5000)
|
||||
@@ -0,0 +1,96 @@
|
||||
import os
|
||||
import sqlite3
|
||||
import hashlib
|
||||
import base64
|
||||
|
||||
def hash_password(password, salt=None):
|
||||
if salt is None:
|
||||
salt = base64.b64encode(os.urandom(16)).decode('utf-8')
|
||||
salted = (salt + password).encode('utf-8')
|
||||
hashed = hashlib.sha256(salted).hexdigest()
|
||||
return f'{salt}${hashed}'
|
||||
|
||||
def verify_password(password, hashed):
|
||||
try:
|
||||
salt, hash_val = hashed.split('$', 1)
|
||||
except ValueError:
|
||||
return False
|
||||
return hash_password(password, salt) == hashed
|
||||
|
||||
def get_db_connection():
|
||||
conn = sqlite3.connect('db/subnets.db')
|
||||
conn.row_factory = sqlite3.Row
|
||||
conn.execute('PRAGMA foreign_keys = ON;')
|
||||
return conn
|
||||
|
||||
def init_db():
|
||||
os.makedirs('db', exist_ok=True)
|
||||
conn = sqlite3.connect('db/subnets.db')
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS User (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
email TEXT NOT NULL UNIQUE,
|
||||
password TEXT NOT NULL
|
||||
)
|
||||
''')
|
||||
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS AuditLog (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER,
|
||||
action TEXT NOT NULL,
|
||||
details TEXT,
|
||||
subnet_id INTEGER,
|
||||
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES User(id),
|
||||
FOREIGN KEY (subnet_id) REFERENCES Subnet(id)
|
||||
)
|
||||
''')
|
||||
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS Subnet (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
cidr TEXT NOT NULL,
|
||||
site TEXT
|
||||
)
|
||||
''')
|
||||
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS IPAddress (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
ip TEXT NOT NULL,
|
||||
hostname TEXT,
|
||||
subnet_id INTEGER NOT NULL,
|
||||
FOREIGN KEY (subnet_id) REFERENCES Subnet (id)
|
||||
)
|
||||
''')
|
||||
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS Device (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT
|
||||
)
|
||||
''')
|
||||
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS DeviceIPAddress (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
device_id INTEGER NOT NULL,
|
||||
ip_id INTEGER NOT NULL,
|
||||
FOREIGN KEY (device_id) REFERENCES Device (id),
|
||||
FOREIGN KEY (ip_id) REFERENCES IPAddress (id)
|
||||
)
|
||||
''')
|
||||
|
||||
cursor.execute('SELECT COUNT(*) FROM User')
|
||||
if cursor.fetchone()[0] == 0:
|
||||
cursor.execute('''INSERT INTO User (name, email, password) VALUES (?, ?, ?)''',
|
||||
('Jamie Banks', 'jamie@jdbnet.co.uk', hash_password('Drippy-Cavity-Jawline')))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
@@ -0,0 +1,87 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: ipam
|
||||
namespace: ipam
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: ipam
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: ipam
|
||||
spec:
|
||||
containers:
|
||||
- name: ipam
|
||||
image: docker.jdbnet.co.uk/internal/ipam:latest
|
||||
imagePullPolicy: Always
|
||||
volumeMounts:
|
||||
- mountPath: /app/db
|
||||
name: ipam
|
||||
ports:
|
||||
- containerPort: 5000
|
||||
name: "ipam"
|
||||
livenessProbe:
|
||||
failureThreshold: 3
|
||||
httpGet:
|
||||
path: /
|
||||
port: 5000
|
||||
scheme: HTTP
|
||||
periodSeconds: 10
|
||||
successThreshold: 1
|
||||
timeoutSeconds: 1
|
||||
readinessProbe:
|
||||
failureThreshold: 3
|
||||
httpGet:
|
||||
path: /
|
||||
port: 5000
|
||||
scheme: HTTP
|
||||
periodSeconds: 10
|
||||
successThreshold: 1
|
||||
timeoutSeconds: 1
|
||||
startupProbe:
|
||||
failureThreshold: 3
|
||||
httpGet:
|
||||
path: /
|
||||
port: 5000
|
||||
scheme: HTTP
|
||||
periodSeconds: 10
|
||||
successThreshold: 1
|
||||
timeoutSeconds: 1
|
||||
volumes:
|
||||
- name: ipam
|
||||
persistentVolumeClaim:
|
||||
claimName: ipam
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: ipam-ingress-service
|
||||
namespace: ipam
|
||||
spec:
|
||||
selector:
|
||||
app: ipam
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
targetPort: 5000
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: ipam-ingress
|
||||
namespace: ipam
|
||||
spec:
|
||||
rules:
|
||||
- host: ipam.jdb143.uk
|
||||
http:
|
||||
paths:
|
||||
- pathType: Prefix
|
||||
path: "/"
|
||||
backend:
|
||||
service:
|
||||
name: ipam-ingress-service
|
||||
port:
|
||||
number: 80
|
||||
@@ -0,0 +1,2 @@
|
||||
Flask==2.3.2
|
||||
Flask-WTF==1.1.1
|
||||
@@ -0,0 +1,442 @@
|
||||
from flask import render_template, request, redirect, url_for, send_from_directory, send_file, session
|
||||
from db import init_db, hash_password, get_db_connection, verify_password
|
||||
import sqlite3
|
||||
from ipaddress import ip_network
|
||||
from functools import wraps
|
||||
import os
|
||||
import csv
|
||||
from io import StringIO, BytesIO
|
||||
|
||||
app = None
|
||||
|
||||
def login_required(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if 'logged_in' not in session:
|
||||
return redirect(url_for('login'))
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
|
||||
def add_audit_log(user_id, action, details=None, subnet_id=None, conn=None):
|
||||
close_conn = False
|
||||
if conn is None:
|
||||
conn = get_db_connection()
|
||||
close_conn = True
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''INSERT INTO AuditLog (user_id, action, details, subnet_id) VALUES (?, ?, ?, ?)''',
|
||||
(user_id, action, details, subnet_id))
|
||||
if close_conn:
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def register_routes(app):
|
||||
@app.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
error = None
|
||||
if request.method == 'POST':
|
||||
email = request.form['email']
|
||||
password = request.form['password']
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('SELECT id, password FROM User WHERE email = ?', (email,))
|
||||
user = cursor.fetchone()
|
||||
if user and verify_password(password, user[1]):
|
||||
session['logged_in'] = True
|
||||
session['user_id'] = user[0]
|
||||
return redirect(url_for('index'))
|
||||
else:
|
||||
error = 'Invalid email or password.'
|
||||
return render_with_user('login.html', error=error)
|
||||
|
||||
@app.route('/logout')
|
||||
def logout():
|
||||
session.clear()
|
||||
return redirect(url_for('login'))
|
||||
|
||||
@app.route('/')
|
||||
@login_required
|
||||
def index():
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('SELECT id, name, cidr, site FROM Subnet')
|
||||
subnets = cursor.fetchall()
|
||||
sites_subnets = {}
|
||||
for subnet in subnets:
|
||||
site = subnet[3] or 'Unassigned'
|
||||
if site not in sites_subnets:
|
||||
sites_subnets[site] = []
|
||||
sites_subnets[site].append({'id': subnet[0], 'name': subnet[1], 'cidr': subnet[2]})
|
||||
return render_with_user('index.html', sites_subnets=sites_subnets)
|
||||
|
||||
@app.route('/devices')
|
||||
@login_required
|
||||
def devices():
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('SELECT id, name FROM Device')
|
||||
devices = cursor.fetchall()
|
||||
cursor.execute('SELECT id, name, cidr, site FROM Subnet')
|
||||
subnets = cursor.fetchall()
|
||||
cursor.execute('SELECT DeviceIPAddress.device_id, IPAddress.id, IPAddress.ip FROM DeviceIPAddress JOIN IPAddress ON DeviceIPAddress.ip_id = IPAddress.id')
|
||||
device_ips = {}
|
||||
for row in cursor.fetchall():
|
||||
device_ips.setdefault(row[0], []).append((row[1], row[2]))
|
||||
sites_devices = {}
|
||||
for device in devices:
|
||||
cursor.execute('''SELECT Subnet.site FROM DeviceIPAddress JOIN IPAddress ON DeviceIPAddress.ip_id = IPAddress.id JOIN Subnet ON IPAddress.subnet_id = Subnet.id WHERE DeviceIPAddress.device_id = ? LIMIT 1''', (device[0],))
|
||||
site = cursor.fetchone()
|
||||
site = site[0] if site else 'Unassigned'
|
||||
if site not in sites_devices:
|
||||
sites_devices[site] = []
|
||||
sites_devices[site].append({'id': device[0], 'name': device[1]})
|
||||
return render_with_user('devices.html', sites_devices=sites_devices, device_ips=device_ips)
|
||||
|
||||
@app.route('/add_device', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def add_device():
|
||||
if request.method == 'POST':
|
||||
name = request.form['device_name']
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('INSERT INTO Device (name) VALUES (?)', (name,))
|
||||
conn.commit()
|
||||
return redirect(url_for('devices'))
|
||||
return render_with_user('add_device.html')
|
||||
|
||||
@app.route('/device/<int:device_id>')
|
||||
@login_required
|
||||
def device(device_id):
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('SELECT id, name, description FROM Device WHERE id = ?', (device_id,))
|
||||
device = cursor.fetchone()
|
||||
cursor.execute('SELECT id, name, cidr, site FROM Subnet')
|
||||
subnets = [dict(id=row[0], name=row[1], cidr=row[2], site=row[3]) for row in cursor.fetchall()]
|
||||
cursor.execute('''SELECT DeviceIPAddress.id as device_ip_id, IPAddress.ip FROM DeviceIPAddress JOIN IPAddress ON DeviceIPAddress.ip_id = IPAddress.id WHERE DeviceIPAddress.device_id = ?''', (device_id,))
|
||||
device_ips = [{'device_ip_id': row[0], 'ip': row[1]} for row in cursor.fetchall()]
|
||||
return render_with_user('device.html', device={'id': device[0], 'name': device[1], 'description': device[2]}, subnets=subnets, device_ips=device_ips)
|
||||
|
||||
@app.route('/device/<int:device_id>/add_ip', methods=['POST'])
|
||||
@login_required
|
||||
def device_add_ip(device_id):
|
||||
subnet_id = request.form['subnet_id']
|
||||
ip_id = request.form['ip_id']
|
||||
user_id = session.get('user_id')
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('INSERT INTO DeviceIPAddress (device_id, ip_id) VALUES (?, ?)', (device_id, ip_id))
|
||||
cursor.execute('SELECT name FROM Device WHERE id = ?', (device_id,))
|
||||
device_name = cursor.fetchone()[0]
|
||||
cursor.execute('UPDATE IPAddress SET hostname = ? WHERE id = ?', (device_name, ip_id))
|
||||
cursor.execute('SELECT ip, subnet_id FROM IPAddress WHERE id = ?', (ip_id,))
|
||||
ip, subnet_id_val = cursor.fetchone()
|
||||
cursor.execute('SELECT name, cidr FROM Subnet WHERE id = ?', (subnet_id_val,))
|
||||
subnet_name, subnet_cidr = cursor.fetchone()
|
||||
details = f"Assigned IP {ip} ({subnet_name} {subnet_cidr}) to device {device_name}"
|
||||
add_audit_log(user_id, 'device_add_ip', details, subnet_id_val, conn=conn)
|
||||
conn.commit()
|
||||
return redirect(url_for('device', device_id=device_id))
|
||||
|
||||
@app.route('/device/<int:device_id>/delete_ip', methods=['POST'])
|
||||
@login_required
|
||||
def device_delete_ip(device_id):
|
||||
device_ip_id = request.form['device_ip_id']
|
||||
user_id = session.get('user_id')
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('SELECT ip_id FROM DeviceIPAddress WHERE id = ?', (device_ip_id,))
|
||||
ip_id = cursor.fetchone()[0]
|
||||
cursor.execute('SELECT ip, subnet_id FROM IPAddress WHERE id = ?', (ip_id,))
|
||||
ip, subnet_id_val = cursor.fetchone()
|
||||
cursor.execute('SELECT name, cidr FROM Subnet WHERE id = ?', (subnet_id_val,))
|
||||
subnet_name, subnet_cidr = cursor.fetchone()
|
||||
cursor.execute('SELECT device_id FROM DeviceIPAddress WHERE id = ?', (device_ip_id,))
|
||||
device_id_val = cursor.fetchone()[0]
|
||||
cursor.execute('SELECT name FROM Device WHERE id = ?', (device_id_val,))
|
||||
device_name = cursor.fetchone()[0]
|
||||
details = f"Removed IP {ip} ({subnet_name} {subnet_cidr}) from device {device_name}"
|
||||
add_audit_log(user_id, 'device_delete_ip', details, subnet_id_val, conn=conn)
|
||||
cursor.execute('DELETE FROM DeviceIPAddress WHERE id = ?', (device_ip_id,))
|
||||
cursor.execute('UPDATE IPAddress SET hostname = NULL WHERE id = ?', (ip_id,))
|
||||
conn.commit()
|
||||
return redirect(url_for('device', device_id=device_id))
|
||||
|
||||
@app.route('/delete_device', methods=['POST'])
|
||||
@login_required
|
||||
def delete_device():
|
||||
device_id = request.form['device_id']
|
||||
user_id = session.get('user_id')
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('SELECT name FROM Device WHERE id = ?', (device_id,))
|
||||
device_name = cursor.fetchone()[0]
|
||||
add_audit_log(user_id, 'delete_device', f"Deleted device {device_name}", conn=conn)
|
||||
cursor.execute('DELETE FROM DeviceIPAddress WHERE device_id = ?', (device_id,))
|
||||
cursor.execute('DELETE FROM Device WHERE id = ?', (device_id,))
|
||||
conn.commit()
|
||||
return redirect(url_for('devices'))
|
||||
|
||||
@app.route('/subnet/<int:subnet_id>')
|
||||
@login_required
|
||||
def subnet(subnet_id):
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('SELECT id, name, cidr FROM Subnet WHERE id = ?', (subnet_id,))
|
||||
subnet = cursor.fetchone()
|
||||
cursor.execute('SELECT * FROM IPAddress WHERE subnet_id = ?', (subnet_id,))
|
||||
ip_addresses = cursor.fetchall()
|
||||
cursor.execute('SELECT id, name, description FROM Device')
|
||||
devices = cursor.fetchall()
|
||||
device_name_map = {name.lower(): (id, description) for id, name, description in devices}
|
||||
ip_addresses_with_device = []
|
||||
for ip in ip_addresses:
|
||||
hostname = ip[2]
|
||||
device_id = None
|
||||
device_description = None
|
||||
if hostname:
|
||||
match = device_name_map.get(hostname.lower())
|
||||
if match:
|
||||
device_id, device_description = match
|
||||
ip_addresses_with_device.append((ip[0], ip[1], hostname, device_id, device_description))
|
||||
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
|
||||
def add_subnet():
|
||||
name = request.form['name']
|
||||
cidr = request.form['cidr']
|
||||
site = request.form['site']
|
||||
user_id = session.get('user_id')
|
||||
try:
|
||||
network = ip_network(cidr, strict=False)
|
||||
if network.prefixlen < 24:
|
||||
return render_with_user('admin.html', subnets=[], error='Subnet must be /24 or smaller (e.g., /24, /25, ... /32)')
|
||||
except Exception as e:
|
||||
return render_with_user('admin.html', subnets=[], error='Invalid CIDR format.')
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('INSERT INTO Subnet (name, cidr, site) VALUES (?, ?, ?)', (name, cidr, site))
|
||||
subnet_id = cursor.lastrowid
|
||||
ip_rows = [(str(ip), subnet_id) for ip in network.hosts()]
|
||||
cursor.executemany('INSERT INTO IPAddress (ip, subnet_id) VALUES (?, ?)', ip_rows)
|
||||
add_audit_log(user_id, 'add_subnet', f"Added subnet {name} ({cidr})", subnet_id, conn=conn)
|
||||
conn.commit()
|
||||
return redirect(url_for('admin'))
|
||||
|
||||
@app.route('/delete_subnet', methods=['POST'])
|
||||
@login_required
|
||||
def delete_subnet():
|
||||
subnet_id = request.form['subnet_id']
|
||||
user_id = session.get('user_id')
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('SELECT name, cidr FROM Subnet WHERE id = ?', (subnet_id,))
|
||||
subnet = cursor.fetchone()
|
||||
add_audit_log(user_id, 'delete_subnet', f"Deleted subnet {subnet[0]} ({subnet[1]})", subnet_id, conn=conn)
|
||||
cursor.execute('SELECT id FROM IPAddress WHERE subnet_id = ?', (subnet_id,))
|
||||
ip_ids = [row[0] for row in cursor.fetchall()]
|
||||
if ip_ids:
|
||||
cursor.executemany('DELETE FROM DeviceIPAddress WHERE ip_id = ?', [(ip_id,) for ip_id in ip_ids])
|
||||
cursor.executemany('UPDATE AuditLog SET subnet_id=NULL WHERE subnet_id = ?', [(subnet_id,) for _ in ip_ids])
|
||||
cursor.execute('DELETE FROM IPAddress WHERE subnet_id = ?', (subnet_id,))
|
||||
cursor.execute('DELETE FROM Subnet WHERE id = ?', (subnet_id,))
|
||||
conn.commit()
|
||||
return redirect(url_for('admin'))
|
||||
|
||||
@app.route('/admin', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def admin():
|
||||
with get_db_connection() 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)
|
||||
|
||||
@app.route('/download_db')
|
||||
@login_required
|
||||
def download_db():
|
||||
return send_file('db/subnets.db', as_attachment=True)
|
||||
|
||||
@app.route('/users', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def users():
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
if request.method == 'POST':
|
||||
action = request.form['action']
|
||||
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 (?, ?, ?)', (name, email, password))
|
||||
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=?, email=?, password=? WHERE id=?', (name, email, password, user_id))
|
||||
else:
|
||||
cursor.execute('UPDATE User SET name=?, email=? WHERE id=?', (name, email, user_id))
|
||||
elif action == 'delete':
|
||||
user_id = request.form['user_id']
|
||||
cursor.execute('UPDATE User SET name=? WHERE id=?', ('Deleted User', user_id))
|
||||
cursor.execute('UPDATE AuditLog SET user_id=NULL WHERE user_id=?', (user_id,))
|
||||
cursor.execute('DELETE FROM User WHERE id=?', (user_id,))
|
||||
conn.commit()
|
||||
cursor.execute('SELECT id, name, email FROM User')
|
||||
users = cursor.fetchall()
|
||||
return render_with_user('users.html', users=users)
|
||||
|
||||
@app.route('/audit')
|
||||
@login_required
|
||||
def audit():
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
user_id = request.args.get('user_id')
|
||||
subnet_id = request.args.get('subnet_id')
|
||||
action = request.args.get('action')
|
||||
device_name = request.args.get('device_name')
|
||||
query = '''SELECT AuditLog.id, COALESCE(User.name, 'Deleted User'), AuditLog.action, AuditLog.details, Subnet.name, AuditLog.timestamp FROM AuditLog LEFT JOIN User ON AuditLog.user_id = User.id LEFT JOIN Subnet ON AuditLog.subnet_id = Subnet.id WHERE 1=1'''
|
||||
params = []
|
||||
if user_id:
|
||||
query += ' AND AuditLog.user_id = ?'
|
||||
params.append(user_id)
|
||||
if subnet_id:
|
||||
query += ' AND AuditLog.subnet_id = ?'
|
||||
params.append(subnet_id)
|
||||
if action:
|
||||
query += ' AND AuditLog.action = ?'
|
||||
params.append(action)
|
||||
if device_name:
|
||||
query += ' AND AuditLog.details LIKE ?'
|
||||
params.append(f'%{device_name}%')
|
||||
query += ' ORDER BY AuditLog.timestamp DESC'
|
||||
cursor.execute(query, params)
|
||||
logs = cursor.fetchall()
|
||||
cursor.execute('SELECT id, name FROM User')
|
||||
users = cursor.fetchall()
|
||||
cursor.execute('SELECT id, name FROM Subnet')
|
||||
subnets = cursor.fetchall()
|
||||
cursor.execute('SELECT DISTINCT action FROM AuditLog')
|
||||
actions = [row[0] for row in cursor.fetchall()]
|
||||
cursor.execute('SELECT name FROM Device ORDER BY name')
|
||||
devices = cursor.fetchall()
|
||||
return render_with_user('audit.html', logs=logs, users=users, subnets=subnets, actions=actions, devices=devices)
|
||||
|
||||
@app.route('/get_available_ips')
|
||||
@login_required
|
||||
def get_available_ips():
|
||||
subnet_id = request.args.get('subnet_id')
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''SELECT id, ip FROM IPAddress WHERE subnet_id = ? AND id NOT IN (SELECT ip_id FROM DeviceIPAddress)''', (subnet_id,))
|
||||
available_ips = [{'id': row[0], 'ip': row[1]} for row in cursor.fetchall()]
|
||||
return {'available_ips': available_ips}
|
||||
|
||||
@app.route('/rename_device', methods=['POST'])
|
||||
@login_required
|
||||
def rename_device():
|
||||
device_id = request.form['device_id']
|
||||
new_name = request.form['new_name']
|
||||
user_id = session.get('user_id')
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('SELECT name FROM Device WHERE id = ?', (device_id,))
|
||||
old_name = cursor.fetchone()[0]
|
||||
cursor.execute('UPDATE Device SET name = ? WHERE id = ?', (new_name, device_id))
|
||||
cursor.execute('UPDATE IPAddress SET hostname = ? WHERE hostname = ?', (new_name, old_name))
|
||||
conn.commit()
|
||||
add_audit_log(user_id, 'rename_device', f"Renamed device '{old_name}' to '{new_name}'", conn=conn)
|
||||
return redirect(url_for('device', device_id=device_id))
|
||||
|
||||
@app.route('/update_device_description', methods=['POST'])
|
||||
@login_required
|
||||
def update_device_description():
|
||||
device_id = request.form['device_id']
|
||||
description = request.form['description']
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('UPDATE Device SET description = ? WHERE id = ?', (description, device_id))
|
||||
conn.commit()
|
||||
return redirect(url_for('device', device_id=device_id))
|
||||
|
||||
@app.route('/subnet/<int:subnet_id>/export_csv')
|
||||
@login_required
|
||||
def export_subnet_csv(subnet_id):
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('SELECT id, name, cidr FROM Subnet WHERE id = ?', (subnet_id,))
|
||||
subnet = cursor.fetchone()
|
||||
if not subnet:
|
||||
return 'Subnet not found', 404
|
||||
cursor.execute('SELECT * FROM IPAddress WHERE subnet_id = ?', (subnet_id,))
|
||||
ip_addresses = cursor.fetchall()
|
||||
cursor.execute('SELECT id, name, description FROM Device')
|
||||
devices = cursor.fetchall()
|
||||
device_name_map = {name.lower(): (id, description) for id, name, description in devices}
|
||||
ip_addresses_with_device = []
|
||||
for ip in ip_addresses:
|
||||
hostname = ip[2]
|
||||
device_id = None
|
||||
device_description = None
|
||||
if hostname:
|
||||
match = device_name_map.get(hostname.lower())
|
||||
if match:
|
||||
device_id, device_description = match
|
||||
ip_addresses_with_device.append((ip[0], ip[1], hostname, device_id, device_description))
|
||||
output = StringIO()
|
||||
writer = csv.writer(output)
|
||||
writer.writerow(['IP Address', 'Hostname', 'Description'])
|
||||
for ip in ip_addresses_with_device:
|
||||
ip_addr = ip[1] or ''
|
||||
hostname = ip[2] or ''
|
||||
description = (ip[4] or '').split('\n')[0] if ip[4] else ''
|
||||
writer.writerow([ip_addr, hostname, description])
|
||||
csv_bytes = output.getvalue().encode('utf-8')
|
||||
output_bytes = BytesIO(csv_bytes)
|
||||
output_bytes.seek(0)
|
||||
filename = f"{subnet[1]}_{subnet[2]}_subnet.csv".replace(' ', '_')
|
||||
return send_file(
|
||||
output_bytes,
|
||||
mimetype='text/csv',
|
||||
as_attachment=True,
|
||||
download_name=filename
|
||||
)
|
||||
|
||||
def get_current_user_name():
|
||||
user_id = session.get('user_id')
|
||||
if not user_id:
|
||||
return ''
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('SELECT name FROM User WHERE id = ?', (user_id,))
|
||||
row = cursor.fetchone()
|
||||
return row[0] if row else ''
|
||||
|
||||
def render_with_user(*args, **kwargs):
|
||||
if 'current_user_name' not in kwargs:
|
||||
kwargs['current_user_name'] = get_current_user_name()
|
||||
return render_template(*args, **kwargs)
|
||||
|
||||
app.add_url_rule('/login', 'login', login, methods=['GET', 'POST'])
|
||||
app.add_url_rule('/logout', 'logout', logout)
|
||||
app.add_url_rule('/', 'index', index)
|
||||
app.add_url_rule('/devices', 'devices', devices)
|
||||
app.add_url_rule('/add_device', 'add_device', add_device, methods=['GET', 'POST'])
|
||||
app.add_url_rule('/device/<int:device_id>', 'device', device)
|
||||
app.add_url_rule('/device/<int:device_id>/add_ip', 'device_add_ip', device_add_ip, methods=['POST'])
|
||||
app.add_url_rule('/device/<int:device_id>/delete_ip', 'device_delete_ip', device_delete_ip, methods=['POST'])
|
||||
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('/delete_subnet', 'delete_subnet', delete_subnet, methods=['POST'])
|
||||
app.add_url_rule('/admin', 'admin', admin, methods=['GET', 'POST'])
|
||||
app.add_url_rule('/download_db', 'download_db', download_db)
|
||||
app.add_url_rule('/users', 'users', users, methods=['GET', 'POST'])
|
||||
app.add_url_rule('/audit', 'audit', audit)
|
||||
app.add_url_rule('/get_available_ips', 'get_available_ips', get_available_ips)
|
||||
app.add_url_rule('/rename_device', 'rename_device', rename_device, methods=['POST'])
|
||||
app.add_url_rule('/update_device_description', 'update_device_description', update_device_description, methods=['POST'])
|
||||
app.add_url_rule('/subnet/<int:subnet_id>/export_csv', 'export_subnet_csv', export_subnet_csv)
|
||||
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "Generating CSS..."
|
||||
./tailwindcss -i ./static/css/input.css -o ./static/css/output.css --content "./templates/*.html,./static/js/*.js" --minify
|
||||
|
||||
echo "Starting app..."
|
||||
python app.py
|
||||
@@ -0,0 +1,2 @@
|
||||
sonar.projectKey=internal_ipam.jdb143.uk_9d4cf316-69a9-4ae2-ac2f-29f2ba041432
|
||||
sonar.qualitygate.wait=true
|
||||
@@ -0,0 +1,16 @@
|
||||
h2 {
|
||||
cursor: pointer;
|
||||
}
|
||||
form:not(.mb-6), .mt-4 {
|
||||
display: none;
|
||||
}
|
||||
.allocated-ips {
|
||||
display: block;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.button-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 1rem;
|
||||
justify-items: center;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
@import "tailwindcss"
|
||||
@@ -0,0 +1,15 @@
|
||||
function validateSubnetForm() {
|
||||
const cidrInput = document.getElementById('cidr-input');
|
||||
const errorSpan = document.getElementById('cidr-error');
|
||||
const cidrPattern = /^(?:\d{1,3}\.){3}\d{1,3}\/([0-9]|[1-2][0-9]|3[0-2])$/;
|
||||
if (!cidrPattern.test(cidrInput.value.trim())) {
|
||||
errorSpan.textContent = 'Please enter a valid CIDR (e.g., 192.168.1.0/24)';
|
||||
errorSpan.classList.remove('hidden');
|
||||
cidrInput.classList.add('border-red-500');
|
||||
return false;
|
||||
}
|
||||
errorSpan.textContent = '';
|
||||
errorSpan.classList.add('hidden');
|
||||
cidrInput.classList.remove('border-red-500');
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const siteSelect = document.getElementById('site-select');
|
||||
const subnetSelect = document.getElementById('subnet-select');
|
||||
const ipSelect = document.getElementById('ip-select');
|
||||
const renameBtn = document.querySelector('.rename-btn');
|
||||
const saveBtn = document.querySelector('.save-btn');
|
||||
const cancelBtn = document.querySelector('.cancel-btn');
|
||||
const nameInput = document.querySelector('input[name="new_name"]');
|
||||
const h1 = document.querySelector('h1');
|
||||
siteSelect.addEventListener('change', function() {
|
||||
const selectedSite = this.value;
|
||||
let firstSubnet = null;
|
||||
Array.from(subnetSelect.options).forEach(option => {
|
||||
if (!option.value) return;
|
||||
if (option.getAttribute('data-site') === selectedSite) {
|
||||
option.style.display = '';
|
||||
if (!firstSubnet) firstSubnet = option.value;
|
||||
} else {
|
||||
option.style.display = 'none';
|
||||
}
|
||||
});
|
||||
subnetSelect.value = firstSubnet || '';
|
||||
const event = new Event('change', { bubbles: true });
|
||||
subnetSelect.dispatchEvent(event);
|
||||
});
|
||||
subnetSelect.addEventListener('change', function() {
|
||||
const subnetId = this.value;
|
||||
if (!subnetId) {
|
||||
ipSelect.innerHTML = '<option value="" disabled selected>Select IP</option>';
|
||||
return;
|
||||
}
|
||||
fetch(`/get_available_ips?subnet_id=${subnetId}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
ipSelect.innerHTML = '<option value="" disabled selected>Select IP</option>';
|
||||
data.available_ips.forEach(ip => {
|
||||
const option = document.createElement('option');
|
||||
option.value = ip.id;
|
||||
option.textContent = ip.ip;
|
||||
ipSelect.appendChild(option);
|
||||
});
|
||||
});
|
||||
});
|
||||
if (renameBtn && saveBtn && cancelBtn && nameInput && h1) {
|
||||
renameBtn.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
nameInput.classList.remove('hidden');
|
||||
saveBtn.classList.remove('hidden');
|
||||
cancelBtn.classList.remove('hidden');
|
||||
h1.classList.add('hidden');
|
||||
nameInput.focus();
|
||||
});
|
||||
cancelBtn.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
nameInput.classList.add('hidden');
|
||||
saveBtn.classList.add('hidden');
|
||||
cancelBtn.classList.add('hidden');
|
||||
h1.classList.remove('hidden');
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,110 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
// Expand/collapse site groups
|
||||
document.querySelectorAll('.site-header').forEach(header => {
|
||||
header.addEventListener('click', function(e) {
|
||||
const deviceList = this.closest('.site-group').querySelector('.device-list');
|
||||
const icon = this.querySelector('.expand-btn i');
|
||||
if (deviceList.classList.contains('hidden')) {
|
||||
deviceList.classList.remove('hidden');
|
||||
icon.classList.remove('fa-chevron-down');
|
||||
icon.classList.add('fa-chevron-up');
|
||||
} else {
|
||||
deviceList.classList.add('hidden');
|
||||
icon.classList.remove('fa-chevron-up');
|
||||
icon.classList.add('fa-chevron-down');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Search functionality
|
||||
const searchInput = document.getElementById('search');
|
||||
searchInput.addEventListener('keypress', function(event) {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
const query = this.value.toLowerCase();
|
||||
document.querySelectorAll('.site-group').forEach(siteGroup => {
|
||||
let anyVisible = false;
|
||||
siteGroup.querySelectorAll('.device-list li').forEach(li => {
|
||||
const deviceName = li.querySelector('span').textContent.toLowerCase();
|
||||
const ipSpans = li.querySelectorAll('span.inline-block');
|
||||
let match = deviceName.includes(query);
|
||||
if (!match) {
|
||||
ipSpans.forEach(ipSpan => {
|
||||
if (ipSpan.textContent.toLowerCase().includes(query)) {
|
||||
match = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
li.style.display = match ? '' : 'none';
|
||||
const card = li.querySelector('a');
|
||||
if (match) {
|
||||
anyVisible = true;
|
||||
siteGroup.querySelector('.device-list').classList.remove('hidden');
|
||||
const icon = siteGroup.querySelector('.expand-btn i');
|
||||
if (icon && icon.classList.contains('fa-chevron-down')) {
|
||||
icon.classList.remove('fa-chevron-down');
|
||||
icon.classList.add('fa-chevron-up');
|
||||
}
|
||||
if (card) {
|
||||
card.style.transition = 'background-color 0.3s';
|
||||
card.style.backgroundColor = '#2563eb';
|
||||
card.style.color = '#fff';
|
||||
setTimeout(() => {
|
||||
card.style.backgroundColor = '';
|
||||
card.style.color = '';
|
||||
}, 2000);
|
||||
}
|
||||
} else {
|
||||
if (card) {
|
||||
card.style.backgroundColor = '';
|
||||
card.style.color = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
siteGroup.style.display = anyVisible ? '' : 'none';
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Scroll to Top Button
|
||||
const scrollToTopButton = document.createElement('button');
|
||||
scrollToTopButton.innerHTML = '<i class="fas fa-arrow-up"></i>';
|
||||
scrollToTopButton.style.fontSize = '26px';
|
||||
scrollToTopButton.className = 'fixed bottom-5 right-5 bg-gray-800 text-white p-3 rounded-full shadow-lg hidden';
|
||||
scrollToTopButton.style.width = '60px';
|
||||
scrollToTopButton.style.height = '60px';
|
||||
scrollToTopButton.style.borderRadius = '50%';
|
||||
document.body.appendChild(scrollToTopButton);
|
||||
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
@keyframes bob {
|
||||
0%, 100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
}
|
||||
|
||||
.bobbing {
|
||||
animation: bob 1.5s infinite;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
scrollToTopButton.classList.add('bobbing');
|
||||
|
||||
window.addEventListener('scroll', () => {
|
||||
if (window.scrollY > 200) {
|
||||
scrollToTopButton.classList.remove('hidden');
|
||||
} else {
|
||||
scrollToTopButton.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
scrollToTopButton.addEventListener('click', () => {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
document.querySelectorAll('.export-csv-btn').forEach(btn => {
|
||||
btn.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
const subnetId = this.getAttribute('data-subnet-id');
|
||||
window.location.href = `/subnet/${subnetId}/export_csv`;
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const navToggle = document.getElementById('nav-toggle');
|
||||
const mobileNav = document.getElementById('mobile-nav');
|
||||
navToggle.addEventListener('click', function() {
|
||||
mobileNav.classList.toggle('hidden');
|
||||
});
|
||||
document.addEventListener('click', function(e) {
|
||||
if (!mobileNav.contains(e.target) && !navToggle.contains(e.target)) {
|
||||
mobileNav.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.querySelectorAll('.site-header').forEach(header => {
|
||||
header.addEventListener('click', function(e) {
|
||||
if (e.target.closest('button')) return;
|
||||
const subnetList = this.closest('.site-group').querySelector('.subnet-list');
|
||||
const icon = this.querySelector('.expand-btn i');
|
||||
if (subnetList.classList.contains('hidden')) {
|
||||
subnetList.classList.remove('hidden');
|
||||
icon.classList.remove('fa-chevron-down');
|
||||
icon.classList.add('fa-chevron-up');
|
||||
} else {
|
||||
subnetList.classList.add('hidden');
|
||||
icon.classList.remove('fa-chevron-up');
|
||||
icon.classList.add('fa-chevron-down');
|
||||
}
|
||||
});
|
||||
});
|
||||
document.querySelectorAll('.expand-btn').forEach(btn => {
|
||||
btn.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
const subnetList = this.closest('.site-group').querySelector('.subnet-list');
|
||||
const icon = this.querySelector('i');
|
||||
if (subnetList.classList.contains('hidden')) {
|
||||
subnetList.classList.remove('hidden');
|
||||
icon.classList.remove('fa-chevron-down');
|
||||
icon.classList.add('fa-chevron-up');
|
||||
} else {
|
||||
subnetList.classList.add('hidden');
|
||||
icon.classList.remove('fa-chevron-up');
|
||||
icon.classList.add('fa-chevron-down');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const form = document.querySelector('form');
|
||||
if (form) {
|
||||
form.addEventListener('submit', (event) => {
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
const searchInput = document.createElement('input');
|
||||
searchInput.type = 'text';
|
||||
searchInput.placeholder = 'Search by IP or Hostname';
|
||||
searchInput.className = 'p-2 w-full rounded-lg bg-gray-800 text-gray-100 border border-gray-600 focus:outline-none focus:border-blue-400 mb-4 text-center';
|
||||
form.insertAdjacentElement('beforebegin', searchInput);
|
||||
|
||||
searchInput.addEventListener('keypress', (event) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
const searchTerm = searchInput.value.toLowerCase();
|
||||
const rows = document.querySelectorAll('tbody tr');
|
||||
|
||||
rows.forEach(row => {
|
||||
const ipCell = row.querySelector('td:nth-child(1)').textContent.toLowerCase();
|
||||
const hostnameCell = row.querySelector('td:nth-child(2)').textContent.toLowerCase();
|
||||
|
||||
if (ipCell.includes(searchTerm) || hostnameCell.includes(searchTerm)) {
|
||||
row.style.backgroundColor = 'rgba(59, 130, 246, 0.5)';
|
||||
row.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
|
||||
setTimeout(() => {
|
||||
row.style.backgroundColor = '';
|
||||
}, 3000);
|
||||
} else {
|
||||
row.style.backgroundColor = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Scroll to Top Button
|
||||
const scrollToTopButton = document.createElement('button');
|
||||
scrollToTopButton.innerHTML = '<i class="fas fa-arrow-up"></i>';
|
||||
scrollToTopButton.style.fontSize = '26px';
|
||||
scrollToTopButton.className = 'fixed bottom-5 right-5 bg-gray-800 text-white p-3 rounded-full shadow-lg hidden';
|
||||
scrollToTopButton.style.width = '60px';
|
||||
scrollToTopButton.style.height = '60px';
|
||||
scrollToTopButton.style.borderRadius = '50%';
|
||||
document.body.appendChild(scrollToTopButton);
|
||||
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
@keyframes bob {
|
||||
0%, 100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
}
|
||||
|
||||
.bobbing {
|
||||
animation: bob 1.5s infinite;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
scrollToTopButton.classList.add('bobbing');
|
||||
|
||||
window.addEventListener('scroll', () => {
|
||||
if (window.scrollY > 200) {
|
||||
scrollToTopButton.classList.remove('hidden');
|
||||
} else {
|
||||
scrollToTopButton.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
scrollToTopButton.addEventListener('click', () => {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
});
|
||||
});
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
@@ -0,0 +1,26 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Add Device</title>
|
||||
<link rel="icon" type="image/png" href="/static/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-900 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="flex items-center mb-6 relative">
|
||||
<a href="javascript:window.history.back()" class="absolute left-0 bg-gradient-to-r from-gray-500 to-gray-600 hover:from-gray-600 hover:to-gray-500 text-white flex items-center justify-center rounded-full w-11 h-11"><i class="fas fa-arrow-left"></i></a>
|
||||
<h1 class="text-3xl font-bold text-center w-full">Add Device</h1>
|
||||
</div>
|
||||
<form action="/add_device" method="POST" class="flex flex-col space-y-4">
|
||||
<input type="text" name="device_name" placeholder="Device Name" class="border p-3 rounded-lg bg-gray-800 text-gray-100 border-gray-600" required>
|
||||
<button type="submit" class="bg-gradient-to-r from-gray-500 to-gray-600 hover:from-gray-600 hover:to-gray-500 text-white px-4 py-2 rounded-lg">Add Device</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,56 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Admin Panel</title>
|
||||
<link rel="icon" type="image/png" href="/static/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-900 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">
|
||||
<h1 class="text-3xl font-bold mb-6 text-center">Admin Panel</h1>
|
||||
<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 %}
|
||||
<div class="text-red-500 text-center mb-4">{{ 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-800 text-gray-100 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-800 text-gray-100 border-gray-600" required>
|
||||
<input type="text" name="site" placeholder="Site/Location" class="border p-3 rounded-lg bg-gray-800 text-gray-100 border-gray-600" required>
|
||||
<button type="submit" class="bg-gradient-to-r from-gray-500 to-gray-700 hover:from-gray-600 hover:to-gray-800 text-white px-4 py-2 rounded-lg">Add Subnet</button>
|
||||
<span id="cidr-error" class="text-red-500 text-sm hidden"></span>
|
||||
</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-800 text-gray-100 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 bg-gray-900 rounded-full p-3" title="Delete Subnet">
|
||||
<i class="fas fa-trash fa-lg"></i>
|
||||
</button>
|
||||
</form>
|
||||
<hr class="border-t-2 border-gray-600 rounded-lg mb-4">
|
||||
<h1 class="text-2xl font-bold mb-6 text-center">Database Management</h1>
|
||||
<div class="mb-6 flex justify-center">
|
||||
<a href="/download_db" class="bg-gradient-to-r from-gray-500 to-gray-700 hover:from-gray-600 hover:to-gray-800 text-white px-4 py-2 rounded-lg">Download Database</a>
|
||||
</div>
|
||||
<form action="/admin" method="POST" enctype="multipart/form-data" class="flex flex-col space-y-4">
|
||||
<input type="file" name="file" class="border p-3 rounded-lg bg-gray-800 text-gray-100 border-gray-600" required>
|
||||
<button type="submit" class="bg-gradient-to-r from-gray-500 to-gray-700 hover:from-gray-600 hover:to-gray-800 text-white px-4 py-2 rounded-lg">Upload Database</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/static/js/add_subnet.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,79 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Audit Log</title>
|
||||
<link rel="icon" type="image/png" href="/static/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-900 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">Audit Log</h1>
|
||||
<form method="GET" class="flex flex-wrap gap-4 mb-6 justify-center">
|
||||
<select name="user_id" class="border p-2 rounded-lg bg-gray-800 text-gray-100 border-gray-600">
|
||||
<option value="">All Users</option>
|
||||
{% for user in users %}
|
||||
<option value="{{ user[0] }}" {% if request.args.get('user_id') == user[0]|string %}selected{% endif %}>{{ user[1] }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<select name="subnet_id" class="border p-2 rounded-lg bg-gray-800 text-gray-100 border-gray-600">
|
||||
<option value="">All Subnets</option>
|
||||
{% for subnet in subnets %}
|
||||
<option value="{{ subnet[0] }}" {% if request.args.get('subnet_id') == subnet[0]|string %}selected{% endif %}>{{ subnet[1] }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<select name="action" class="border p-2 rounded-lg bg-gray-800 text-gray-100 border-gray-600">
|
||||
<option value="">All Actions</option>
|
||||
{% for a in actions %}
|
||||
<option value="{{ a }}" {% if request.args.get('action') == a %}selected{% endif %}>{{ a }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<select name="device_name" class="border p-2 rounded-lg bg-gray-800 text-gray-100 border-gray-600">
|
||||
<option value="">All Devices</option>
|
||||
{% for device in devices %}
|
||||
<option value="{{ device[0] }}" {% if request.args.get('device_name') == device[0] %}selected{% endif %}>{{ device[0] }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button type="submit" class="bg-gradient-to-r from-gray-500 to-gray-600 hover:from-gray-600 hover:to-gray-500 text-white px-4 py-2 rounded-lg">Filter</button>
|
||||
</form>
|
||||
<table class="w-full table-auto bg-gray-800 rounded-lg overflow-hidden">
|
||||
<thead>
|
||||
<tr class="bg-gray-700">
|
||||
<th class="px-4 py-2 text-center">User</th>
|
||||
<th class="px-4 py-2 text-center">Action</th>
|
||||
<th class="px-4 py-2 text-center">Details</th>
|
||||
<th class="px-4 py-2 text-center">Subnet</th>
|
||||
<th class="px-4 py-2 text-center">Timestamp</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for log in logs %}
|
||||
<tr class="border-b border-gray-700">
|
||||
<td class="px-4 py-2 text-center">{{ log[1] or 'Unknown' }}</td>
|
||||
<td class="px-4 py-2 text-center">{{ log[2] }}</td>
|
||||
<td class="px-4 py-2 text-center">{{ log[3] }}</td>
|
||||
<td class="px-4 py-2 text-center">{{ log[4] or 'N/A' }}</td>
|
||||
<td class="px-4 py-2 text-center" data-utc="{{ log[5] }}">{{ log[5] }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.querySelectorAll('td[data-utc]').forEach(function(td) {
|
||||
const utc = td.getAttribute('data-utc');
|
||||
if (utc) {
|
||||
const date = new Date(utc + 'Z');
|
||||
td.textContent = date.toLocaleString();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,79 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ device.name }} - Device Details</title>
|
||||
<link rel="icon" type="image/png" href="/static/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-900 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 w-auto min-w-[20rem] max-w-2xl pt-20">
|
||||
<div class="flex items-center mb-8 relative justify-between gap-4">
|
||||
<a href="javascript:window.history.back()" class="bg-gradient-to-r from-gray-500 to-gray-600 hover:from-gray-600 hover:to-gray-500 text-white flex items-center justify-center rounded-full w-11 h-11 shrink-0"><i class="fas fa-arrow-left"></i></a>
|
||||
<h1 class="text-3xl font-bold text-center flex-1 min-w-0 truncate">{{ device.name }}</h1>
|
||||
<div class="flex items-center shrink-0">
|
||||
<form action="/rename_device" method="POST" class="inline">
|
||||
<input type="hidden" name="device_id" value="{{ device.id }}">
|
||||
<input type="text" name="new_name" value="{{ device.name }}" class="hidden border p-1 rounded bg-gray-800 text-gray-100 border-gray-600 w-32 mr-2" style="vertical-align: middle;" required>
|
||||
<button type="button" class="text-blue-400 hover:text-blue-600 ml-2 rename-btn" title="Rename Device"><i class="fas fa-pencil-alt"></i></button>
|
||||
<button type="submit" class="text-green-400 hover:text-green-600 ml-2 save-btn hidden" title="Save Name"><i class="fas fa-check"></i></button>
|
||||
<button type="button" class="text-gray-400 hover:text-gray-600 ml-2 cancel-btn hidden" title="Cancel"><i class="fas fa-times"></i></button>
|
||||
</form>
|
||||
<form action="/delete_device" method="POST" onsubmit="return confirm('Are you sure you want to delete this device?');">
|
||||
<input type="hidden" name="device_id" value="{{ device.id }}">
|
||||
<button type="submit" class="ml-4 text-red-500 hover:text-red-700" title="Delete Device">
|
||||
<i class="fas fa-trash fa-lg"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<form action="/device/{{ device.id }}/add_ip" method="POST" class="mb-6">
|
||||
<div class="flex flex-col space-y-4">
|
||||
<select name="site" id="site-select" class="border p-3 rounded-lg bg-gray-800 text-gray-100 border-gray-600 w-full" required>
|
||||
<option value="" disabled selected>Select Site...</option>
|
||||
{% set sites = subnets | map(attribute='site') | unique | list %}
|
||||
{% for site in sites %}
|
||||
<option value="{{ site }}">{{ site }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<select name="subnet_id" id="subnet-select" class="border p-3 rounded-lg bg-gray-800 text-gray-100 border-gray-600 w-full" required>
|
||||
<option value="" disabled selected>Select Subnet...</option>
|
||||
{% for subnet in subnets %}
|
||||
<option value="{{ subnet.id }}" data-site="{{ subnet.site }}">{{ subnet.name }} ({{ subnet.cidr }})</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<select name="ip_id" id="ip-select" class="border p-3 rounded-lg bg-gray-800 text-gray-100 border-gray-600 w-full" required>
|
||||
<option value="" disabled selected>Select IP...</option>
|
||||
</select>
|
||||
<button type="submit" class="bg-gradient-to-r from-gray-500 to-gray-600 hover:from-gray-600 hover:to-gray-500 text-white px-4 py-2 rounded-lg w-full">Add IP</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="allocated-ips">
|
||||
<h3 class="text-lg font-bold mb-2">Allocated IPs:</h3>
|
||||
<ul class="space-y-2">
|
||||
{% for ip in device_ips %}
|
||||
<li class="flex justify-between items-center bg-gray-700 p-2 rounded-lg">
|
||||
<span class="allocated-ip">{{ ip.ip }}</span>
|
||||
<form action="/device/{{ device.id }}/delete_ip" method="POST" class="inline">
|
||||
<input type="hidden" name="device_ip_id" value="{{ ip.device_ip_id }}">
|
||||
<button type="submit" class="bg-gradient-to-r from-red-500 to-red-700 hover:from-red-600 hover:to-red-800 text-white px-2 py-1 rounded-lg">Delete</button>
|
||||
</form>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<form action="/update_device_description" method="POST" class="mb-6 mt-4">
|
||||
<input type="hidden" name="device_id" value="{{ device.id }}">
|
||||
<label for="description" class="block mb-2 text-lg font-bold">Description</label>
|
||||
<textarea id="description" name="description" rows="3" class="border p-2 rounded-lg bg-gray-800 text-gray-100 border-gray-600 w-full resize-y" placeholder="Enter device description...">{{ device.description or '' }}</textarea>
|
||||
<button type="submit" class="bg-gradient-to-r from-gray-500 to-gray-600 hover:from-gray-600 hover:to-gray-500 text-white px-4 py-2 rounded-lg w-full mt-2">Save Description</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/static/js/device.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,56 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Device Manager</title>
|
||||
<link rel="icon" type="image/png" href="/static/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">
|
||||
<link href="/static/css/devices.css" rel="stylesheet">
|
||||
</head>
|
||||
<body class="bg-gray-900 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-4xl pt-20">
|
||||
<h1 class="text-3xl font-bold mb-6 text-center">Device Manager</h1>
|
||||
<a href="/add_device" class="block mb-6 text-center bg-gradient-to-r from-gray-500 to-gray-600 hover:from-gray-600 hover:to-gray-500 text-white px-4 py-2 rounded-lg">Add New Device</a>
|
||||
<div class="mb-6">
|
||||
<input type="text" id="search" placeholder="Search devices or IPs..." class="border p-3 rounded-lg bg-gray-800 text-gray-100 border-gray-600 w-full">
|
||||
</div>
|
||||
<div id="site-list" class="space-y-6">
|
||||
{% for site, devices in sites_devices.items() %}
|
||||
<div class="site-group bg-gray-800 rounded-lg shadow-md">
|
||||
<div class="flex flex-row items-center justify-between p-4 cursor-pointer site-header">
|
||||
<h2 class="text-xl font-bold mb-0 text-blue-300">{{ site }}</h2>
|
||||
<button type="button" class="expand-btn text-gray-400 hover:text-gray-200 ml-2 flex items-center" aria-label="Expand site">
|
||||
<i class="fas fa-chevron-down"></i>
|
||||
</button>
|
||||
</div>
|
||||
<ul class="device-list hidden px-6 pb-4">
|
||||
{% for device in devices %}
|
||||
<li class="my-2">
|
||||
<a href="/device/{{ device.id }}" class="flex items-center justify-between bg-gray-900 hover:bg-gray-700 text-gray-200 hover:text-white font-semibold rounded-lg px-4 py-2 shadow transition-colors duration-150">
|
||||
<span><i class="fas fa-server mr-2"></i>{{ device.name }}</span>
|
||||
{% set ips = device_ips.get(device.id, []) %}
|
||||
<span class="flex flex-row flex-wrap justify-end items-center ml-4 text-xs text-blue-300 font-normal align-middle">
|
||||
{% if ips|length > 0 %}
|
||||
{% for ip in ips %}
|
||||
<span class="inline-block bg-gray-800 text-blue-200 rounded px-2 py-1 ml-1 font-mono">{{ ip[1] }}</span>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<span class="text-gray-400">No IPs</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/static/js/devices.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,33 @@
|
||||
<header class="bg-gray-800 shadow-md py-3 px-6 flex items-center justify-between relative">
|
||||
<a href="/" class="flex items-center space-x-3">
|
||||
<img src="/static/logo.png" alt="Logo" class="h-8 rounded">
|
||||
<span class="text-2xl font-bold text-white">JDB-NET IPAM</span>
|
||||
</a>
|
||||
<div class="hidden lg:block absolute left-1/2 transform -translate-x-1/2 text-white text-lg font-medium whitespace-nowrap">
|
||||
{% if current_user_name %}Hello, {{ current_user_name }}{% endif %}
|
||||
</div>
|
||||
<nav class="hidden md:flex items-center space-x-6" id="main-nav">
|
||||
<a href="/" class="text-gray-200 hover:text-blue-400 font-medium">Home</a>
|
||||
<a href="/devices" class="text-gray-200 hover:text-blue-400 font-medium">Devices</a>
|
||||
<a href="/admin" class="text-gray-200 hover:text-blue-400 font-medium">Admin</a>
|
||||
<a href="/audit" class="text-gray-200 hover:text-blue-400 font-medium">Audit Log</a>
|
||||
{% if current_user_name %}
|
||||
<a href="/logout" class="text-gray-200 hover:text-blue-400 font-medium">Logout</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
<button class="md:hidden flex items-center text-gray-200 focus:outline-none" id="nav-toggle" aria-label="Open navigation menu">
|
||||
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="md:hidden absolute top-16 right-6 bg-gray-800 rounded-lg shadow-lg z-50 w-48 hidden flex-col py-2" id="mobile-nav">
|
||||
<a href="/" class="block px-6 py-2 text-gray-200 hover:text-blue-400 font-medium">Home</a>
|
||||
<a href="/devices" class="block px-6 py-2 text-gray-200 hover:text-blue-400 font-medium">Devices</a>
|
||||
<a href="/admin" class="block px-6 py-2 text-gray-200 hover:text-blue-400 font-medium">Admin</a>
|
||||
<a href="/audit" class="block px-6 py-2 text-gray-200 hover:text-blue-400 font-medium">Audit Log</a>
|
||||
{% if current_user_name %}
|
||||
<a href="/logout" class="block px-6 py-2 text-gray-200 hover:text-blue-400 font-medium">Logout</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<script src="/static/js/header.js"></script>
|
||||
</header>
|
||||
@@ -0,0 +1,46 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>JDB-NET IPAM</title>
|
||||
<link rel="icon" type="image/png" href="/static/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-900 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">
|
||||
<h1 class="text-3xl font-bold mb-6 text-center">JDB-NET IPAM</h1>
|
||||
<ul class="space-y-4">
|
||||
{% for site, subnets in sites_subnets.items() %}
|
||||
<li class="site-group bg-gray-800 rounded-xl shadow-lg">
|
||||
<div class="flex flex-row items-center justify-between p-4 cursor-pointer site-header">
|
||||
<h2 class="text-xl font-bold mb-0 text-blue-300">{{ site }}</h2>
|
||||
<button type="button" class="expand-btn text-gray-400 hover:text-gray-200 ml-2 flex items-center" aria-label="Expand site">
|
||||
<i class="fas fa-chevron-down"></i>
|
||||
</button>
|
||||
</div>
|
||||
<ul class="subnet-list hidden space-y-4 px-2 pb-4">
|
||||
{% for subnet in subnets %}
|
||||
<li class="p-4 bg-gray-900 rounded-lg shadow-md flex items-center justify-between">
|
||||
<div>
|
||||
<a href="/subnet/{{ subnet.id }}" class="text-blue-400 hover:text-blue-300 text-lg font-medium">{{ subnet.name }}</a>
|
||||
<p class="text-sm text-gray-400">{{ subnet.cidr }}</p>
|
||||
</div>
|
||||
<button type="button" class="export-csv-btn ml-2 bg-gradient-to-r from-gray-500 to-gray-600 hover:from-gray-600 hover:to-gray-500 text-white flex items-center justify-center rounded-full w-9 h-9" title="Export as CSV" data-subnet-id="{{ subnet.id }}">
|
||||
<i class="fas fa-file-csv"></i>
|
||||
</button>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<script src="/static/js/sitelist.js"></script>
|
||||
<script src="/static/js/export_csv.js"></script>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,26 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>JDB-NET IPAM</title>
|
||||
<link rel="icon" type="image/png" href="/static/logo.png">
|
||||
<link href="/static/css/output.css" rel="stylesheet">
|
||||
</head>
|
||||
<body class="bg-gray-900 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">
|
||||
<h1 class="text-3xl font-bold mb-6 text-center">JDB-NET IPAM</h1>
|
||||
<form action="/login" method="POST" class="flex flex-col space-y-4">
|
||||
<input type="email" name="email" placeholder="Email Address" class="border p-3 rounded-lg bg-gray-800 text-gray-100 border-gray-600" required>
|
||||
<input type="password" name="password" placeholder="Password" class="border p-3 rounded-lg bg-gray-800 text-gray-100 border-gray-600" required>
|
||||
<button type="submit" class="bg-gradient-to-r from-gray-500 to-gray-600 hover:from-gray-600 hover:to-gray-500 text-white px-4 py-2 rounded-lg">Login</button>
|
||||
</form>
|
||||
{% if error %}
|
||||
<p class="text-red-500 text-center mt-4">{{ error }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,57 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ subnet.name }} - Subnet Details</title>
|
||||
<link rel="icon" type="image/png" href="/static/logo.png">
|
||||
<link href="/static/css/output.css" rel="stylesheet">
|
||||
<script src="/static/js/subnet.js"></script>
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css" rel="stylesheet">
|
||||
</head>
|
||||
<body class="bg-gray-900 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-3/4 pt-20">
|
||||
<div class="flex items-center mb-6 relative">
|
||||
<a href="javascript:window.history.back()" class="absolute left-0 bg-gradient-to-r from-gray-500 to-gray-600 hover:from-gray-600 hover:to-gray-500 text-white flex items-center justify-center rounded-full w-11 h-11"><i class="fas fa-arrow-left"></i></a>
|
||||
<h1 class="text-3xl font-bold text-center w-full">{{ subnet.name }} ({{ subnet.cidr }})</h1>
|
||||
<button type="button" id="export-csv" class="absolute right-0 bg-gradient-to-r from-gray-500 to-gray-600 hover:from-gray-600 hover:to-gray-500 text-white flex items-center justify-center rounded-full w-11 h-11 export-csv-btn" title="Export as CSV" data-subnet-id="{{ subnet.id }}">
|
||||
<i class="fas fa-file-csv fa-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
<form action="" method="POST">
|
||||
<table class="table-auto w-full mb-6">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center text-gray-400">IP Address</th>
|
||||
<th class="text-center text-gray-400">Hostname</th>
|
||||
<th class="text-center text-gray-400">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-700">
|
||||
{% for ip in ip_addresses %}
|
||||
<tr>
|
||||
<td class="text-gray-300 font-bold text-center">{{ ip[1] }}</td>
|
||||
<td class="text-white text-center">
|
||||
{% if ip[2] and ip[3] %}
|
||||
<a href="/device/{{ ip[3] }}" class="hover:text-blue-300">{{ ip[2] }}</a>
|
||||
{% elif ip[2] %}
|
||||
{{ ip[2] }}
|
||||
{% else %}
|
||||
{{ '' }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-white text-left align-top">
|
||||
<textarea readonly rows="1" class="bg-gray-800 text-gray-100 border border-gray-600 rounded w-full resize-y cursor-pointer p-2">{{ ip[4].split('\n')[0] if ip[4] else '' }}</textarea>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/static/js/export_csv.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,48 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>User Management</title>
|
||||
<link rel="icon" type="image/png" href="/static/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-900 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-800 p-6 rounded-lg shadow-md">
|
||||
<input type="hidden" name="action" value="add">
|
||||
<div class="flex flex-col space-y-4">
|
||||
<input type="text" name="name" placeholder="Name" class="border p-3 rounded-lg bg-gray-900 text-gray-100 border-gray-600" required>
|
||||
<input type="email" name="email" placeholder="Email Address" class="border p-3 rounded-lg bg-gray-900 text-gray-100 border-gray-600" required>
|
||||
<input type="password" name="password" placeholder="Password" class="border p-3 rounded-lg bg-gray-900 text-gray-100 border-gray-600" required>
|
||||
<button type="submit" class="bg-gradient-to-r from-gray-500 to-gray-600 hover:from-gray-600 hover:to-gray-500 text-white px-4 py-2 rounded-lg">Add User</button>
|
||||
</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-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-900 text-gray-100 border-gray-600 w-52">
|
||||
<input type="email" name="email" value="{{ user[2] }}" class="border p-2 rounded-lg bg-gray-900 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-900 text-gray-100 border-gray-600 w-80">
|
||||
<button type="submit" class="bg-gradient-to-r from-gray-500 to-gray-600 hover:from-gray-600 hover:to-gray-500 text-white 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 ml-2" title="Delete User"><i class="fas fa-trash"></i></button>
|
||||
</form>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user