LOTS of changes
This commit is contained in:
BIN
app/__pycache__/config.cpython-39.pyc
Normal file
BIN
app/__pycache__/config.cpython-39.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/database.cpython-39.pyc
Normal file
BIN
app/__pycache__/database.cpython-39.pyc
Normal file
Binary file not shown.
537
app/app.py
537
app/app.py
@@ -1,531 +1,30 @@
|
||||
from flask import Flask, render_template, request, redirect, url_for, jsonify
|
||||
import mysql.connector
|
||||
import json
|
||||
from flask import Flask, redirect, url_for, render_template
|
||||
from views.index_views import index
|
||||
from views.user_views import user
|
||||
from views.group_views import group
|
||||
from config import app_config
|
||||
from database import init_app
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config.from_object(app_config)
|
||||
|
||||
DB_CONFIG = {
|
||||
'host': '192.168.60.150',
|
||||
'user': 'user_92z0Kj',
|
||||
'password': '5B3UXZV8vyrB',
|
||||
'database': 'radius_NIaIuT'
|
||||
}
|
||||
init_app(app)
|
||||
|
||||
def get_db():
|
||||
try:
|
||||
db = mysql.connector.connect(**DB_CONFIG)
|
||||
return db
|
||||
except mysql.connector.Error as err:
|
||||
print(f"Database Connection Error: {err}")
|
||||
return None
|
||||
|
||||
@app.route('/', methods=['GET', 'POST'])
|
||||
def index():
|
||||
sql_results = None
|
||||
sql_error = None
|
||||
total_users = 0
|
||||
total_groups = 0
|
||||
|
||||
db = get_db()
|
||||
if db:
|
||||
cursor = db.cursor(dictionary=True)
|
||||
try:
|
||||
cursor.execute("SELECT COUNT(DISTINCT username) as total FROM radcheck;")
|
||||
total_users = cursor.fetchone()['total']
|
||||
|
||||
cursor.execute("SELECT COUNT(DISTINCT groupname) as total FROM radgroupreply;")
|
||||
total_groups = cursor.fetchone()['total']
|
||||
|
||||
except mysql.connector.Error as err:
|
||||
print(f"Error fetching counts: {err}")
|
||||
|
||||
cursor.close()
|
||||
db.close()
|
||||
|
||||
return render_template('index.html', total_users=total_users, total_groups=total_groups, sql_results=sql_results, sql_error=sql_error)
|
||||
|
||||
@app.route('/sql', methods=['POST'])
|
||||
def sql():
|
||||
sql_results = None
|
||||
sql_error = None
|
||||
sql_query = request.form['query']
|
||||
|
||||
db = get_db()
|
||||
if db:
|
||||
try:
|
||||
cursor = db.cursor(dictionary=True)
|
||||
cursor.execute(sql_query)
|
||||
sql_results = cursor.fetchall()
|
||||
cursor.close()
|
||||
db.close()
|
||||
except mysql.connector.Error as err:
|
||||
sql_error = str(err)
|
||||
except Exception as e:
|
||||
sql_error = str(e)
|
||||
|
||||
total_users = 0
|
||||
total_groups = 0
|
||||
|
||||
db = get_db()
|
||||
if db:
|
||||
cursor = db.cursor(dictionary=True)
|
||||
try:
|
||||
cursor.execute("SELECT COUNT(DISTINCT username) as total FROM radcheck;")
|
||||
total_users = cursor.fetchone()['total']
|
||||
|
||||
cursor.execute("SELECT COUNT(DISTINCT groupname) as total FROM radgroupreply;")
|
||||
total_groups = cursor.fetchone()['total']
|
||||
|
||||
except mysql.connector.Error as err:
|
||||
print(f"Error fetching counts: {err}")
|
||||
|
||||
cursor.close()
|
||||
db.close()
|
||||
|
||||
return render_template('index.html', total_users=total_users, total_groups=total_groups, sql_results=sql_results, sql_error=sql_error)
|
||||
app.register_blueprint(index)
|
||||
app.register_blueprint(user, url_prefix='/user')
|
||||
app.register_blueprint(group, url_prefix='/group')
|
||||
|
||||
@app.route('/user_list')
|
||||
def user_list():
|
||||
db = get_db()
|
||||
if db is None:
|
||||
return "Database connection failed", 500
|
||||
|
||||
cursor = db.cursor(dictionary=True)
|
||||
try:
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
r.username AS mac_address,
|
||||
rd.description AS description,
|
||||
ug.groupname AS vlan_id
|
||||
FROM radcheck r
|
||||
LEFT JOIN radusergroup ug ON r.username = ug.username
|
||||
LEFT JOIN rad_description rd ON r.username = rd.username
|
||||
""")
|
||||
results = cursor.fetchall()
|
||||
print("Results:", results)
|
||||
|
||||
cursor.execute("SELECT groupname FROM radgroupcheck")
|
||||
groups = cursor.fetchall()
|
||||
groups = [{'groupname': row['groupname']} for row in groups]
|
||||
print("Groups:", groups)
|
||||
|
||||
cursor.close()
|
||||
db.close()
|
||||
return render_template('user_list_inline_edit.html', results=results, groups=groups)
|
||||
except mysql.connector.Error as e:
|
||||
print(f"Database error: {e}")
|
||||
cursor.close()
|
||||
db.close()
|
||||
return "Database error", 500
|
||||
|
||||
@app.route('/update_user', methods=['POST'])
|
||||
def update_user():
|
||||
mac_address = request.form['mac_address']
|
||||
description = request.form['description']
|
||||
vlan_id = request.form['vlan_id']
|
||||
new_mac_address = request.form.get('new_mac_address')
|
||||
|
||||
print(f"Update request received: mac_address={mac_address}, description={description}, vlan_id={vlan_id}, new_mac_address={new_mac_address}")
|
||||
|
||||
db = get_db()
|
||||
if db:
|
||||
cursor = db.cursor()
|
||||
try:
|
||||
db.autocommit = False
|
||||
|
||||
if new_mac_address and new_mac_address != mac_address:
|
||||
print("Updating MAC address...")
|
||||
# Update radcheck
|
||||
cursor.execute("""
|
||||
UPDATE radcheck
|
||||
SET username = %s, value = %s
|
||||
WHERE username = %s
|
||||
""", (new_mac_address, new_mac_address, mac_address))
|
||||
print(f"radcheck update affected {cursor.rowcount} rows.")
|
||||
|
||||
# Update rad_description
|
||||
cursor.execute("""
|
||||
UPDATE rad_description
|
||||
SET username = %s, description = %s
|
||||
WHERE username = %s
|
||||
""", (new_mac_address, description, mac_address))
|
||||
print(f"rad_description update affected {cursor.rowcount} rows.")
|
||||
|
||||
# Update radusergroup
|
||||
cursor.execute("""
|
||||
UPDATE radusergroup
|
||||
SET username = %s, groupname = %s
|
||||
WHERE username = %s
|
||||
""", (new_mac_address, vlan_id, mac_address))
|
||||
print(f"radusergroup update affected {cursor.rowcount} rows.")
|
||||
|
||||
mac_address = new_mac_address
|
||||
else:
|
||||
print("Updating description and VLAN...")
|
||||
# Update rad_description
|
||||
cursor.execute("""
|
||||
UPDATE rad_description
|
||||
SET description = %s
|
||||
WHERE username = %s
|
||||
""", (description, mac_address))
|
||||
print(f"rad_description update affected {cursor.rowcount} rows.")
|
||||
|
||||
# Update radusergroup
|
||||
cursor.execute("""
|
||||
UPDATE radusergroup
|
||||
SET groupname = %s
|
||||
WHERE username = %s
|
||||
""", (vlan_id, mac_address))
|
||||
print(f"radusergroup update affected {cursor.rowcount} rows.")
|
||||
|
||||
if cursor.rowcount > 0:
|
||||
print("Database rows were modified.")
|
||||
else:
|
||||
print("Database rows were not modified.")
|
||||
|
||||
db.commit()
|
||||
db.autocommit = True
|
||||
cursor.close()
|
||||
print("Update successful")
|
||||
return "success"
|
||||
|
||||
except mysql.connector.Error as err:
|
||||
db.rollback()
|
||||
db.autocommit = True
|
||||
cursor.close()
|
||||
print(f"Database Error: {err}")
|
||||
return str(err)
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
db.autocommit = True
|
||||
cursor.close()
|
||||
print(f"Exception: {e}")
|
||||
return str(e)
|
||||
|
||||
finally:
|
||||
db.close()
|
||||
else:
|
||||
print("Database Connection Failed")
|
||||
return "Database Connection Failed"
|
||||
|
||||
@app.route('/delete_user/<mac_address>')
|
||||
def delete_user(mac_address):
|
||||
db = get_db()
|
||||
if db:
|
||||
cursor = db.cursor()
|
||||
try:
|
||||
db.autocommit = False
|
||||
|
||||
cursor.execute("DELETE FROM rad_description WHERE username = %s", (mac_address,))
|
||||
cursor.execute("DELETE FROM radcheck WHERE username = %s", (mac_address,))
|
||||
cursor.execute("DELETE FROM radusergroup WHERE username = %s", (mac_address,))
|
||||
|
||||
db.commit()
|
||||
db.autocommit = True
|
||||
cursor.close()
|
||||
db.close()
|
||||
return redirect(url_for('user_list'))
|
||||
except mysql.connector.Error as err:
|
||||
print(f"Database Error: {err}")
|
||||
db.rollback()
|
||||
db.autocommit = True
|
||||
cursor.close()
|
||||
db.close()
|
||||
return redirect(url_for('user_list'))
|
||||
return "Database Connection Failed"
|
||||
def legacy_user_list():
|
||||
return redirect(url_for('user.user_list'))
|
||||
|
||||
@app.route('/groups')
|
||||
def groups():
|
||||
db = get_db()
|
||||
if db:
|
||||
cursor = db.cursor()
|
||||
try:
|
||||
cursor.execute("SELECT DISTINCT groupname FROM radgroupcheck")
|
||||
group_names = [row[0] for row in cursor.fetchall()]
|
||||
def legacy_group_list():
|
||||
return redirect(url_for('group.groups'))
|
||||
|
||||
grouped_results = {}
|
||||
for groupname in group_names:
|
||||
cursor.execute("SELECT id, attribute, op, value FROM radgroupreply WHERE groupname = %s", (groupname,))
|
||||
attributes = cursor.fetchall()
|
||||
grouped_results[groupname] = [{'id': row[0], 'attribute': row[1], 'op': row[2], 'value': row[3]} for row in attributes]
|
||||
|
||||
cursor.close()
|
||||
db.close()
|
||||
return render_template('group_list_nested.html', grouped_results=grouped_results)
|
||||
except mysql.connector.Error as err:
|
||||
print(f"Database Error: {err}")
|
||||
cursor.close()
|
||||
db.close()
|
||||
return render_template('group_list_nested.html', grouped_results={})
|
||||
return "Database Connection Failed"
|
||||
|
||||
@app.route('/edit_groupname/<old_groupname>', methods=['GET', 'POST'])
|
||||
def edit_groupname(old_groupname):
|
||||
db = get_db()
|
||||
if db:
|
||||
cursor = db.cursor(dictionary=True)
|
||||
|
||||
if request.method == 'POST':
|
||||
new_groupname = request.form['groupname']
|
||||
try:
|
||||
db.autocommit = False
|
||||
cursor.execute("""
|
||||
UPDATE radgroupreply
|
||||
SET groupname = %s
|
||||
WHERE groupname = %s
|
||||
""", (new_groupname, old_groupname))
|
||||
|
||||
cursor.execute("""
|
||||
UPDATE radusergroup
|
||||
SET groupname = %s
|
||||
WHERE groupname = %s
|
||||
""", (new_groupname, old_groupname))
|
||||
|
||||
db.commit()
|
||||
db.autocommit = True
|
||||
cursor.close()
|
||||
db.close()
|
||||
return redirect(url_for('groups'))
|
||||
except mysql.connector.Error as err:
|
||||
db.rollback()
|
||||
db.autocommit = True
|
||||
cursor.close()
|
||||
db.close()
|
||||
return f"Database Error: {err}"
|
||||
else:
|
||||
return render_template('edit_groupname.html', old_groupname=old_groupname)
|
||||
return "Database Connection Failed"
|
||||
|
||||
@app.route('/update_attribute', methods=['POST'])
|
||||
def update_attribute():
|
||||
group_id = request.form['group_id']
|
||||
attribute = request.form['attribute']
|
||||
op = request.form['op']
|
||||
value = request.form['value']
|
||||
|
||||
db = get_db()
|
||||
if db:
|
||||
cursor = db.cursor()
|
||||
try:
|
||||
db.autocommit = False
|
||||
cursor.execute("""
|
||||
UPDATE radgroupreply
|
||||
SET attribute = %s, op = %s, value = %s
|
||||
WHERE id = %s
|
||||
""", (attribute, op, value, group_id))
|
||||
db.commit()
|
||||
db.autocommit = True
|
||||
cursor.close()
|
||||
return "success"
|
||||
except mysql.connector.Error as err:
|
||||
db.rollback()
|
||||
db.autocommit = True
|
||||
cursor.close()
|
||||
return str(err)
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
db.autocommit = True
|
||||
cursor.close()
|
||||
return str(e)
|
||||
finally:
|
||||
db.close()
|
||||
return "Database Connection Failed"
|
||||
|
||||
@app.route('/add_attribute', methods=['POST'])
|
||||
def add_attribute():
|
||||
groupname = request.form['groupname']
|
||||
attribute = request.form['attribute']
|
||||
op = request.form['op']
|
||||
value = request.form['value']
|
||||
|
||||
db = get_db()
|
||||
if db:
|
||||
cursor = db.cursor()
|
||||
try:
|
||||
cursor.execute("""
|
||||
INSERT INTO radgroupreply (groupname, attribute, op, value)
|
||||
VALUES (%s, %s, %s, %s)
|
||||
""", (groupname, attribute, op, value))
|
||||
db.commit()
|
||||
cursor.close()
|
||||
db.close()
|
||||
return "success"
|
||||
except mysql.connector.Error as err:
|
||||
print(f"Database Error: {err}")
|
||||
db.rollback()
|
||||
cursor.close()
|
||||
db.close()
|
||||
return str(err)
|
||||
return "Database Connection Failed"
|
||||
|
||||
@app.route('/edit_attribute/<group_id>', methods=['GET', 'POST'])
|
||||
def edit_attribute(group_id):
|
||||
db = get_db()
|
||||
if db:
|
||||
cursor = db.cursor(dictionary=True)
|
||||
|
||||
if request.method == 'POST':
|
||||
attribute = request.form['attribute']
|
||||
op = request.form['op']
|
||||
value = request.form['value']
|
||||
|
||||
try:
|
||||
db.autocommit = False
|
||||
cursor.execute("""
|
||||
UPDATE radgroupreply
|
||||
SET attribute = %s, op = %s, value = %s
|
||||
WHERE id = %s
|
||||
""", (attribute, op, value, group_id))
|
||||
db.commit()
|
||||
db.autocommit = True
|
||||
cursor.close()
|
||||
db.close()
|
||||
return redirect(url_for('groups'))
|
||||
except mysql.connector.Error as err:
|
||||
db.rollback()
|
||||
db.autocommit = True
|
||||
cursor.close()
|
||||
db.close()
|
||||
return f"Database Error: {err}"
|
||||
|
||||
else:
|
||||
cursor.execute("SELECT * FROM radgroupreply WHERE id = %s", (group_id,))
|
||||
attribute_data = cursor.fetchone()
|
||||
cursor.close()
|
||||
db.close()
|
||||
return render_template('edit_attribute.html', attribute_data=attribute_data)
|
||||
return "Database Connection Failed"
|
||||
|
||||
@app.route('/add_group', methods=['POST'])
|
||||
def add_group():
|
||||
groupname = request.form['groupname']
|
||||
|
||||
db = get_db()
|
||||
if db:
|
||||
cursor = db.cursor()
|
||||
try:
|
||||
cursor.execute("INSERT INTO radgroupreply (groupname, attribute, op, value) VALUES (%s, '', '', '')", (groupname,))
|
||||
cursor.execute("INSERT INTO radusergroup (groupname, username) VALUES (%s, '')", (groupname,))
|
||||
cursor.execute("INSERT INTO radgroupcheck (groupname, attribute, op, value) VALUES (%s, 'Auth-Type', ':=', 'Accept')", (groupname,))
|
||||
db.commit()
|
||||
cursor.close()
|
||||
db.close()
|
||||
return "success"
|
||||
except mysql.connector.Error as err:
|
||||
print(f"Database Error: {err}")
|
||||
db.rollback()
|
||||
cursor.close()
|
||||
db.close()
|
||||
return str(err)
|
||||
return "Database Connection Failed"
|
||||
|
||||
@app.route('/delete_group_rows/<groupname>')
|
||||
def delete_group_rows(groupname):
|
||||
db = get_db()
|
||||
if db:
|
||||
cursor = db.cursor()
|
||||
try:
|
||||
cursor.execute("DELETE FROM radgroupreply WHERE groupname = %s", (groupname,))
|
||||
cursor.execute("DELETE FROM radusergroup WHERE groupname = %s", (groupname,))
|
||||
cursor.execute("DELETE FROM radgroupcheck WHERE groupname = %s", (groupname,))
|
||||
db.commit()
|
||||
cursor.close()
|
||||
db.close()
|
||||
return redirect(url_for('groups'))
|
||||
except mysql.connector.Error as err:
|
||||
print(f"Database Error: {err}")
|
||||
db.rollback()
|
||||
cursor.close()
|
||||
db.close()
|
||||
return redirect(url_for('groups'))
|
||||
return "Database Connection Failed"
|
||||
|
||||
@app.route('/delete_group/<int:group_id>')
|
||||
def delete_group(group_id):
|
||||
db = get_db()
|
||||
if db:
|
||||
cursor = db.cursor()
|
||||
try:
|
||||
cursor.execute("DELETE FROM radgroupreply WHERE id = %s", (group_id,))
|
||||
cursor.execute("DELETE FROM radgroupcheck WHERE id = %s", (group_id,))
|
||||
db.commit()
|
||||
cursor.close()
|
||||
db.close()
|
||||
return redirect(url_for('groups'))
|
||||
except mysql.connector.Error as err:
|
||||
print(f"Database Error: {err}")
|
||||
db.rollback()
|
||||
cursor.close()
|
||||
db.close()
|
||||
return redirect(url_for('groups'))
|
||||
return "Database Connection Failed"
|
||||
|
||||
@app.route('/add_user', methods=['POST'])
|
||||
def add_user():
|
||||
try:
|
||||
data = request.get_json()
|
||||
mac_address = data.get('mac_address')
|
||||
description = data.get('description')
|
||||
vlan_id = data.get('vlan_id')
|
||||
|
||||
if not mac_address:
|
||||
return jsonify({'success': False, 'message': 'MAC Address is required'}), 400
|
||||
|
||||
db = get_db()
|
||||
if db is None:
|
||||
return jsonify({'success': False, 'message': 'Database connection failed'}), 500
|
||||
|
||||
cursor = db.cursor()
|
||||
try:
|
||||
db.autocommit = False
|
||||
|
||||
cursor.execute("SELECT username FROM radcheck WHERE username = %s", (mac_address,))
|
||||
if cursor.fetchone():
|
||||
cursor.close()
|
||||
db.close()
|
||||
return jsonify({'success': False, 'message': 'User with this MAC Address already exists'}), 400
|
||||
|
||||
cursor.execute("""
|
||||
INSERT INTO radcheck (username, attribute, op, value)
|
||||
VALUES (%s, 'Cleartext-Password', ':=', %s)
|
||||
""", (mac_address, mac_address))
|
||||
|
||||
cursor.execute("""
|
||||
INSERT INTO rad_description (username, description)
|
||||
VALUES (%s, %s)
|
||||
""", (mac_address, description))
|
||||
|
||||
cursor.execute("""
|
||||
INSERT INTO radusergroup (username, groupname)
|
||||
VALUES (%s, %s)
|
||||
""", (mac_address, vlan_id))
|
||||
|
||||
db.commit()
|
||||
db.autocommit = True
|
||||
cursor.close()
|
||||
db.close()
|
||||
return jsonify({'success': True, 'message': 'User added successfully'})
|
||||
|
||||
except mysql.connector.Error as err:
|
||||
print(f"Database Error: {err}")
|
||||
db.rollback()
|
||||
db.autocommit = True
|
||||
cursor.close()
|
||||
db.close()
|
||||
return jsonify({'success': False, 'message': f"Database error: {err}"}), 500
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error adding user: {e}")
|
||||
db.rollback()
|
||||
db.autocommit = True
|
||||
cursor.close()
|
||||
db.close()
|
||||
return jsonify({'success': False, 'message': str(e)}), 500
|
||||
finally:
|
||||
db.close()
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'message': 'Unknown error'}), 500
|
||||
@app.route('/')
|
||||
def index_redirect():
|
||||
return render_template('index.html')
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True, host='0.0.0.0', port=8080)
|
||||
33
app/config.py
Normal file
33
app/config.py
Normal file
@@ -0,0 +1,33 @@
|
||||
import os
|
||||
|
||||
class Config:
|
||||
"""Base configuration."""
|
||||
DEBUG = False
|
||||
TESTING = False
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||
SECRET_KEY = os.getenv('FLASK_SECRET_KEY', 'default-insecure-key')
|
||||
OUI_API_URL = os.getenv('OUI_API_URL', 'https://api.maclookup.app/v2/macs/{}')
|
||||
OUI_API_KEY = os.getenv('OUI_API_KEY', '')
|
||||
OUI_API_LIMIT_PER_SEC = int(os.getenv('OUI_API_LIMIT_PER_SEC', '2'))
|
||||
OUI_API_DAILY_LIMIT = int(os.getenv('OUI_API_DAILY_LIMIT', '10000'))
|
||||
|
||||
class DevelopmentConfig(Config):
|
||||
"""Development configuration."""
|
||||
DEBUG = True
|
||||
MYSQL_HOST = os.getenv('MYSQL_HOST', '192.168.60.150')
|
||||
MYSQL_USER = os.getenv('MYSQL_USER', 'user_92z0Kj')
|
||||
MYSQL_PASSWORD = os.getenv('MYSQL_PASSWORD', '5B3UXZV8vyrB')
|
||||
MYSQL_DATABASE = os.getenv('MYSQL_DATABASE', 'radius_NIaIuT')
|
||||
|
||||
class ProductionConfig(Config):
|
||||
"""Production configuration."""
|
||||
MYSQL_HOST = os.getenv('MYSQL_HOST')
|
||||
MYSQL_USER = os.getenv('MYSQL_USER')
|
||||
MYSQL_PASSWORD = os.getenv('MYSQL_PASSWORD')
|
||||
MYSQL_DATABASE = os.getenv('MYSQL_DATABASE')
|
||||
|
||||
# Use the correct config based on environment
|
||||
if os.getenv('FLASK_ENV') == 'production':
|
||||
app_config = ProductionConfig
|
||||
else:
|
||||
app_config = DevelopmentConfig
|
||||
34
app/database.py
Normal file
34
app/database.py
Normal file
@@ -0,0 +1,34 @@
|
||||
import mysql.connector
|
||||
from flask import current_app, g
|
||||
from mysql.connector import pooling
|
||||
|
||||
# Optional: Use a pool if desired
|
||||
def init_db_pool():
|
||||
return pooling.MySQLConnectionPool(
|
||||
pool_name="mypool",
|
||||
pool_size=5,
|
||||
pool_reset_session=True,
|
||||
host=current_app.config['MYSQL_HOST'],
|
||||
user=current_app.config['MYSQL_USER'],
|
||||
password=current_app.config['MYSQL_PASSWORD'],
|
||||
database=current_app.config['MYSQL_DATABASE']
|
||||
)
|
||||
|
||||
def get_db():
|
||||
if 'db' not in g:
|
||||
if 'db_pool' not in current_app.config:
|
||||
current_app.config['db_pool'] = init_db_pool()
|
||||
g.db = current_app.config['db_pool'].get_connection()
|
||||
return g.db
|
||||
|
||||
def close_db(e=None):
|
||||
db = g.pop('db', None)
|
||||
if db is not None:
|
||||
try:
|
||||
db.close() # returns connection to the pool
|
||||
except Exception as err:
|
||||
print(f"[DB Cleanup] Failed to close DB connection: {err}")
|
||||
|
||||
|
||||
def init_app(app):
|
||||
app.teardown_appcontext(close_db)
|
||||
@@ -1,2 +1,5 @@
|
||||
Flask
|
||||
mysql-connector-python
|
||||
mysql-connector-python
|
||||
requests
|
||||
BeautifulSoup4
|
||||
lxml
|
||||
@@ -1,19 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Add User</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Add User</h1>
|
||||
<form method="POST">
|
||||
<label for="mac_address">MAC Address:</label>
|
||||
<input type="text" name="mac_address" required><br><br>
|
||||
<label for="vlan_id">VLAN ID:</label>
|
||||
<input type="text" name="vlan_id" required><br><br>
|
||||
<label for="description">Description:</label>
|
||||
<input type="text" name="description"><br><br>
|
||||
<input type="submit" value="Add User">
|
||||
</form>
|
||||
<a href="{{ url_for('user_list') }}">Back to User List</a>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,35 +1,239 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{% block title %}{% endblock %}</title>
|
||||
<style>
|
||||
nav {
|
||||
background-color: #f0f0f0;
|
||||
padding: 10px;
|
||||
}
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>{% block title %}FreeRADIUS Manager{% endblock %}</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #ffffff;
|
||||
--fg: #000000;
|
||||
--cell-bg: #f5f5f5;
|
||||
--th-bg: #e0e0e0;
|
||||
}
|
||||
|
||||
nav a {
|
||||
margin-right: 10px;
|
||||
text-decoration: none;
|
||||
padding: 5px 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
}
|
||||
[data-theme="dark"] {
|
||||
--bg: #121212;
|
||||
--fg: #e0e0e0;
|
||||
--cell-bg: #1e1e1e;
|
||||
--th-bg: #2c2c2c;
|
||||
}
|
||||
|
||||
nav a.active {
|
||||
background-color: #ddd;
|
||||
}
|
||||
</style>
|
||||
body {
|
||||
background-color: var(--bg);
|
||||
color: var(--fg);
|
||||
font-family: sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
nav {
|
||||
background-color: var(--th-bg);
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
nav a {
|
||||
margin-right: 10px;
|
||||
text-decoration: none;
|
||||
padding: 5px 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
color: var(--fg);
|
||||
}
|
||||
|
||||
nav a.active {
|
||||
background-color: var(--cell-bg);
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
#theme-toggle {
|
||||
margin-left: auto;
|
||||
cursor: pointer;
|
||||
padding: 5px 10px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #ccc;
|
||||
background-color: var(--cell-bg);
|
||||
color: var(--fg);
|
||||
}
|
||||
|
||||
.styled-table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin-bottom: 2rem;
|
||||
background-color: var(--bg);
|
||||
color: var(--fg);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.styled-table th,
|
||||
.styled-table td {
|
||||
border: 1px solid #444;
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.styled-table input,
|
||||
.styled-table select {
|
||||
background-color: var(--cell-bg);
|
||||
color: var(--fg);
|
||||
border: 1px solid #666;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.styled-table input:focus,
|
||||
.styled-table select:focus {
|
||||
background-color: #555;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.styled-table thead {
|
||||
background-color: var(--th-bg);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.styled-table tbody tr:hover {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.icon-button {
|
||||
background-color: var(--cell-bg);
|
||||
color: var(--fg);
|
||||
border: 1px solid #666;
|
||||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
margin: 2px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
transition: background-color 0.2s, transform 0.2s;
|
||||
}
|
||||
|
||||
.icon-button:hover {
|
||||
background-color: #555;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.icon-button.pulse {
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { box-shadow: 0 0 0 0 rgba(50, 205, 50, 0.4); }
|
||||
70% { box-shadow: 0 0 0 10px rgba(50, 205, 50, 0); }
|
||||
100% { box-shadow: 0 0 0 0 rgba(50, 205, 50, 0); }
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin-bottom: 1rem;
|
||||
color: var(--fg);
|
||||
}
|
||||
|
||||
.merged-cell {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
animation: fadeIn 0.5s ease-in;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(-10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
#toast-container {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.toast {
|
||||
background-color: #444;
|
||||
color: #fff;
|
||||
padding: 10px 16px;
|
||||
margin-top: 10px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 0 10px rgba(0,0,0,0.2);
|
||||
opacity: 0.95;
|
||||
animation: fadein 0.5s, fadeout 0.5s 2.5s;
|
||||
}
|
||||
|
||||
@keyframes fadein {
|
||||
from { opacity: 0; right: 0; }
|
||||
to { opacity: 0.95; }
|
||||
}
|
||||
|
||||
@keyframes fadeout {
|
||||
from { opacity: 0.95; }
|
||||
to { opacity: 0; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav>
|
||||
<a href="/" {% if request.path == '/' %}class="active"{% endif %}>Home</a>
|
||||
<a href="/user_list" {% if request.path == '/user_list' %}class="active"{% endif %}>User List</a>
|
||||
<a href="/groups" {% if request.path == '/groups' %}class="active"{% endif %}>Group List</a>
|
||||
</nav>
|
||||
<nav>
|
||||
<a href="/" {% if request.path == '/' %}class="active"{% endif %}>Home</a>
|
||||
<a href="/user/user_list" {% if request.path.startswith('/user') %}class="active"{% endif %}>User List</a>
|
||||
<a href="/group/groups" {% if request.path.startswith('/group') %}class="active"{% endif %}>Group List</a>
|
||||
<a href="/stats" {% if request.path.startswith('/stats') %}class="active"{% endif %}>Stats</a>
|
||||
<button id="theme-toggle" onclick="toggleTheme()">🌓</button>
|
||||
</nav>
|
||||
|
||||
<div class="content">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
<div class="content">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
|
||||
<div id="toast-container"></div>
|
||||
|
||||
<script>
|
||||
function showToast(message) {
|
||||
const toast = document.createElement('div');
|
||||
toast.textContent = message;
|
||||
toast.style.cssText = `
|
||||
position: fixed;
|
||||
bottom: 1rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: var(--cell-bg, #333);
|
||||
color: var(--fg, #fff);
|
||||
padding: 10px 20px;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
|
||||
z-index: 9999;
|
||||
font-weight: bold;
|
||||
`;
|
||||
document.body.appendChild(toast);
|
||||
setTimeout(() => toast.remove(), 3000);
|
||||
}
|
||||
function toggleTheme() {
|
||||
const current = document.body.dataset.theme || 'light';
|
||||
const next = current === 'light' ? 'dark' : 'light';
|
||||
document.body.dataset.theme = next;
|
||||
localStorage.setItem('theme', next);
|
||||
}
|
||||
|
||||
window.onload = () => {
|
||||
document.body.dataset.theme = localStorage.getItem('theme') || 'light';
|
||||
};
|
||||
</script>
|
||||
|
||||
<script>
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
showToast("{{ message }}");
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Edit Attribute</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Edit Attribute</h1>
|
||||
|
||||
<form method="POST">
|
||||
<label for="attribute">Attribute:</label><br>
|
||||
<input type="text" id="attribute" name="attribute" value="{{ attribute_data.attribute }}"><br><br>
|
||||
|
||||
<label for="op">Op:</label><br>
|
||||
<input type="text" id="op" name="op" value="{{ attribute_data.op }}"><br><br>
|
||||
|
||||
<label for="value">Value:</label><br>
|
||||
<input type="text" id="value" name="value" value="{{ attribute_data.value }}"><br><br>
|
||||
|
||||
<input type="submit" value="Save Changes">
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,26 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Edit Group</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Edit Group: {{ group.id }}</h1>
|
||||
|
||||
<form method="POST">
|
||||
<label for="groupname">Group Name:</label><br>
|
||||
<input type="text" id="groupname" name="groupname" value="{{ group.groupname }}"><br><br>
|
||||
|
||||
<label for="attribute">Attribute:</label><br>
|
||||
<input type="text" id="attribute" name="attribute" value="{{ group.attribute }}"><br><br>
|
||||
|
||||
<label for="op">Op:</label><br>
|
||||
<input type="text" id="op" name="op" value="{{ group.op }}"><br><br>
|
||||
|
||||
<label for="value">Value:</label><br>
|
||||
<input type="text" id="value" name="value" value="{{ group.value }}"><br><br>
|
||||
|
||||
<input type="submit" value="Save Changes">
|
||||
</form>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,16 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Edit Group Name</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Edit Group Name: {{ old_groupname }}</h1>
|
||||
|
||||
<form method="POST">
|
||||
<label for="groupname">New Group Name:</label><br>
|
||||
<input type="text" id="groupname" name="groupname" value="{{ old_groupname }}"><br><br>
|
||||
|
||||
<input type="submit" value="Save Changes">
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,20 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Edit User</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Edit User: {{ user.mac_address }}</h1>
|
||||
|
||||
<form method="POST">
|
||||
<label for="description">Description:</label><br>
|
||||
<input type="text" id="description" name="description" value="{{ user.description }}"><br><br>
|
||||
|
||||
<label for="vlan_id">VLAN ID:</label><br>
|
||||
<input type="text" id="vlan_id" name="vlan_id" value="{{ user.vlan_id }}"><br><br>
|
||||
|
||||
<input type="submit" value="Save Changes">
|
||||
</form>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,38 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Group List</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Group List</h1>
|
||||
|
||||
<table border="1">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Group Name</th>
|
||||
<th>Attribute</th>
|
||||
<th>Op</th>
|
||||
<th>Value</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for group in results %}
|
||||
<tr>
|
||||
<td>{{ group.id }}</td>
|
||||
<td>{{ group.groupname }}</td>
|
||||
<td>{{ group.attribute }}</td>
|
||||
<td>{{ group.op }}</td>
|
||||
<td>{{ group.value }}</td>
|
||||
<td>
|
||||
<a href="/edit_group/{{ group.id }}">Edit</a> |
|
||||
<a href="/delete_group/{{ group.id }}">Delete</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,364 +1,183 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Group List{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Group List</h1>
|
||||
<h1 class="page-title">Group List</h1>
|
||||
|
||||
<table class="styled-table fade-in">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Group Name</th>
|
||||
<th>Attribute</th>
|
||||
<th>Op</th>
|
||||
<th>Value</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="group-body">
|
||||
<!-- New Group Entry Row -->
|
||||
<tr class="new-row">
|
||||
<td rowspan="1"><input type="text" id="new-groupname" placeholder="New group" /></td>
|
||||
<td><input type="text" class="new-attribute" placeholder="Attribute"></td>
|
||||
<td>
|
||||
<select class="new-op">
|
||||
<option value="">Op</option>
|
||||
<option value="=">=</option>
|
||||
<option value="!=">!=</option>
|
||||
<option value=">">></option>
|
||||
<option value="<"><</option>
|
||||
<option value=">=">>=</option>
|
||||
<option value="<="><=</option>
|
||||
</select>
|
||||
</td>
|
||||
<td><input type="text" class="new-value" placeholder="Value"></td>
|
||||
<td>
|
||||
<button class="icon-button pulse" onclick="saveNewGroup()" title="Save Group">💾</button>
|
||||
<button class="icon-button" onclick="addAttributeRow()" title="Add Attribute">➕</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{% for groupname, attributes in grouped_results.items() %}
|
||||
<table border="1">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Group Name</th>
|
||||
<th>Attributes</th>
|
||||
<th>Op</th>
|
||||
<th>Value</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<input type="text" id="groupname-{{ groupname }}" value="{{ groupname }}">
|
||||
</td>
|
||||
<td colspan="3" class="merged-cell">
|
||||
<button onclick="addRow('{{ groupname }}')">➕</button>
|
||||
</td>
|
||||
<td>
|
||||
<button onclick="updateGroupName('{{ groupname }}')">✅ Rename Group</button>
|
||||
<button onclick="location.reload()">❌</button>
|
||||
<a href="/delete_group_rows/{{ groupname }}" onclick="saveScrollPosition()">🗑️</a>
|
||||
<button onclick="duplicateGroup('{{ groupname }}')">Duplicate</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% for attribute in attributes %}
|
||||
<tr>
|
||||
<td class="merged-cell"></td>
|
||||
<td><input type="text" id="attribute-{{ attribute.id }}" value="{{ attribute.attribute }}"></td>
|
||||
<td>
|
||||
<select id="op-{{ attribute.id }}">
|
||||
<option value="=" ${attribute.op === '=' ? 'selected' : ''}>=</option>
|
||||
<option value="!=" ${attribute.op === '!=' ? 'selected' : ''}>!=</option>
|
||||
<option value=">" ${attribute.op === '>' ? 'selected' : ''}>></option>
|
||||
<option value="<" ${attribute.op === '<' ? 'selected' : ''}><</option>
|
||||
<option value=">=" ${attribute.op === '>=' ? 'selected' : ''}>>=</option>
|
||||
<option value="<=" ${attribute.op === '<=' ? 'selected' : ''}><=</option>
|
||||
</select>
|
||||
</td>
|
||||
<td><input type="text" id="value-{{ attribute.id }}" value="{{ attribute.value }}"></td>
|
||||
<td>
|
||||
<button onclick="updateAttribute('{{ attribute.id }}')">✅</button>
|
||||
<button onclick="location.reload()">❌</button>
|
||||
<a href="/delete_group/{{ attribute.id }}" onclick="saveScrollPosition()">🗑️</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<tr>
|
||||
<td><input type="text" id="groupname-{{ groupname }}" value="{{ groupname }}" disabled></td>
|
||||
<td colspan="3" class="merged-cell"></td>
|
||||
<td>
|
||||
<button class="icon-button" onclick="enableEdit('{{ groupname }}')" title="Edit">✏️</button>
|
||||
<button class="icon-button" onclick="updateGroupName('{{ groupname }}')" title="Save">💾</button>
|
||||
<button class="icon-button" onclick="location.reload()" title="Cancel">❌</button>
|
||||
<a class="icon-button" href="{{ url_for('group.delete_group_rows', groupname=groupname) }}" onclick="saveScrollPosition()" title="Delete Group">🗑️</a>
|
||||
<button class="icon-button" onclick="duplicateToNewGroup('{{ groupname }}')" title="Duplicate">📄</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% for attribute in attributes %}
|
||||
<tr>
|
||||
<td class="merged-cell"></td>
|
||||
<td><input type="text" id="attribute-{{ attribute.id }}" value="{{ attribute.attribute }}"></td>
|
||||
<td>
|
||||
<select id="op-{{ attribute.id }}">
|
||||
<option value="=" {% if attribute.op == '=' %}selected{% endif %}>=</option>
|
||||
<option value="!=" {% if attribute.op == '!=' %}selected{% endif %}>!=</option>
|
||||
<option value=">" {% if attribute.op == '>' %}selected{% endif %}>></option>
|
||||
<option value="<" {% if attribute.op == '<' %}selected{% endif %}><</option>
|
||||
<option value=">=" {% if attribute.op == '>=' %}selected{% endif %}>>=</option>
|
||||
<option value="<=" {% if attribute.op == '<=' %}selected{% endif %}><=</option>
|
||||
</select>
|
||||
</td>
|
||||
<td><input type="text" id="value-{{ attribute.id }}" value="{{ attribute.value }}"></td>
|
||||
<td>
|
||||
<button class="icon-button" onclick="updateAttribute('{{ attribute.id }}')" title="Save">💾</button>
|
||||
<button class="icon-button" onclick="location.reload()" title="Reset">❌</button>
|
||||
<a class="icon-button" href="{{ url_for('group.delete_group', group_id=attribute.id) }}" onclick="saveScrollPosition()" title="Delete">🗑️</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table border="1">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Group Name</th>
|
||||
<th>Attributes</th>
|
||||
<th>Op</th>
|
||||
<th>Value</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<input type="text" id="new-groupname" value="">
|
||||
</td>
|
||||
<td colspan="3" class="merged-cell"></td>
|
||||
<td>
|
||||
<button onclick="addNewGroup()">Add New Group</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<script>
|
||||
function enableEdit(groupname) {
|
||||
const input = document.getElementById(`groupname-${groupname}`);
|
||||
input.disabled = false;
|
||||
input.focus();
|
||||
}
|
||||
|
||||
<dialog id="duplicate-dialog">
|
||||
<div id="duplicate-dialog-content"></div>
|
||||
<button id="close-dialog">❌</button>
|
||||
<button id="save-duplicated-group">Save</button>
|
||||
</dialog>
|
||||
function saveScrollPosition() {
|
||||
sessionStorage.setItem("scrollPosition", window.scrollY);
|
||||
}
|
||||
|
||||
<style>
|
||||
.merged-cell {
|
||||
border: none;
|
||||
}
|
||||
</style>
|
||||
function addAttributeRow() {
|
||||
const table = document.getElementById("group-body");
|
||||
const row = document.createElement("tr");
|
||||
row.classList.add("new-attribute-row");
|
||||
row.innerHTML = `
|
||||
<td class="merged-cell"></td>
|
||||
<td><input type="text" class="new-attribute" placeholder="Attribute"></td>
|
||||
<td>
|
||||
<select class="new-op">
|
||||
<option value="">Op</option>
|
||||
<option value="=">=</option>
|
||||
<option value="!=">!=</option>
|
||||
<option value=">">></option>
|
||||
<option value="<"><</option>
|
||||
<option value=">=">>=</option>
|
||||
<option value="<="><=</option>
|
||||
</select>
|
||||
</td>
|
||||
<td><input type="text" class="new-value" placeholder="Value"></td>
|
||||
<td><button class="icon-button" onclick="this.closest('tr').remove()" title="Remove">🗑️</button></td>
|
||||
`;
|
||||
table.insertBefore(row, table.querySelector(".new-row").nextSibling);
|
||||
}
|
||||
|
||||
<script>
|
||||
function updateAttribute(attributeId) {
|
||||
const attribute = document.getElementById(`attribute-${attributeId}`).value;
|
||||
const op = document.getElementById(`op-${attributeId}`).value;
|
||||
const value = document.getElementById(`value-${attributeId}`).value;
|
||||
function saveNewGroup() {
|
||||
const groupname = document.getElementById("new-groupname").value;
|
||||
const attributes = [];
|
||||
const attrInputs = document.querySelectorAll(".new-attribute");
|
||||
|
||||
fetch('/update_attribute', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: `attributeId=${attributeId}&attribute=${attribute}&op=${op}&value=${value}`
|
||||
})
|
||||
.then(response => response.text())
|
||||
.then(data => {
|
||||
if (data === 'success') {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Error updating attribute: ' + data);
|
||||
}
|
||||
});
|
||||
}
|
||||
attrInputs.forEach((attrInput, index) => {
|
||||
const attribute = attrInput.value;
|
||||
const op = document.querySelectorAll(".new-op")[index].value;
|
||||
const value = document.querySelectorAll(".new-value")[index].value;
|
||||
|
||||
function updateGroupName(oldGroupName) {
|
||||
const newGroupName = document.getElementById(`groupname-${oldGroupName}`).value;
|
||||
if (attribute && op && value) {
|
||||
attributes.push({ attribute, op, value });
|
||||
}
|
||||
});
|
||||
|
||||
fetch('/update_group_name', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: `oldGroupName=${oldGroupName}&newGroupName=${newGroupName}`
|
||||
})
|
||||
.then(response => response.text())
|
||||
.then(data => {
|
||||
if (data === 'success') {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Error updating group name: ' + data);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!groupname || attributes.length === 0) {
|
||||
showToast("Group name and at least one attribute required.");
|
||||
return;
|
||||
}
|
||||
|
||||
function addRow(groupName) {
|
||||
const table = event.target.closest('table').querySelector('tbody');
|
||||
const newRow = table.insertRow(table.rows.length);
|
||||
fetch("/group/save_group", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ groupname, attributes })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showToast("Group saved.");
|
||||
setTimeout(() => location.reload(), 800);
|
||||
} else {
|
||||
showToast("Error: " + data.error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const cell1 = newRow.insertCell(0);
|
||||
const cell2 = newRow.insertCell(1);
|
||||
const cell3 = newRow.insertCell(2);
|
||||
const cell4 = newRow.insertCell(3);
|
||||
const cell5 = newRow.insertCell(4);
|
||||
function duplicateToNewGroup(groupname) {
|
||||
fetch("/group/duplicate_group", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: `groupname=${groupname}`
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
document.getElementById("new-groupname").value = data.new_groupname;
|
||||
|
||||
cell1.classList.add('merged-cell');
|
||||
cell2.innerHTML = '<input type="text" id="new-attribute" value="">';
|
||||
cell3.innerHTML = `
|
||||
<select id="new-op">
|
||||
<option value="=">=</option>
|
||||
<option value="!=">!=</option>
|
||||
<option value=">">></option>
|
||||
<option value="<"><</option>
|
||||
<option value=">=">>=</option>
|
||||
<option value="<="><=</option>
|
||||
</select>
|
||||
`;
|
||||
cell4.innerHTML = '<input type="text" id="new-value" value="">';
|
||||
cell5.innerHTML = '<button onclick="saveNewRow(\'' + groupName + '\', this)">✅</button> <button onclick="removeRow(this)">❌</button>';
|
||||
}
|
||||
const oldAttrRows = document.querySelectorAll(".new-attribute-row");
|
||||
oldAttrRows.forEach(row => row.remove());
|
||||
|
||||
function saveNewRow(groupName, button) {
|
||||
const row = button.parentNode.parentNode;
|
||||
const attribute = row.querySelector('#new-attribute').value;
|
||||
const op = row.querySelector('#new-op').value;
|
||||
const value = row.querySelector('#new-value').value;
|
||||
data.attributes.forEach(attr => {
|
||||
addAttributeRow();
|
||||
const index = document.querySelectorAll(".new-attribute").length - 1;
|
||||
document.querySelectorAll(".new-attribute")[index].value = attr.attribute;
|
||||
document.querySelectorAll(".new-op")[index].value = attr.op;
|
||||
document.querySelectorAll(".new-value")[index].value = attr.value;
|
||||
});
|
||||
|
||||
fetch('/add_attribute', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: `groupname=${groupName}&attribute=${attribute}&op=${op}&value=${value}`
|
||||
})
|
||||
.then(response => response.text())
|
||||
.then(data => {
|
||||
if (data === 'success') {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Error adding attribute: ' + data);
|
||||
}
|
||||
});
|
||||
}
|
||||
document.getElementById("new-groupname").scrollIntoView({ behavior: 'smooth' });
|
||||
showToast("Fields populated from duplicated group.");
|
||||
});
|
||||
}
|
||||
|
||||
function removeRow(button) {
|
||||
const row = button.parentNode.parentNode;
|
||||
row.parentNode.removeChild(row);
|
||||
}
|
||||
|
||||
function addNewGroup() {
|
||||
const newGroupName = document.getElementById('new-groupname').value;
|
||||
|
||||
fetch('/add_group', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: `groupname=${newGroupName}`
|
||||
})
|
||||
.then(response => response.text())
|
||||
.then(data => {
|
||||
if (data === 'success') {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Error adding group: ' + data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function saveScrollPosition() {
|
||||
sessionStorage.setItem('scrollPosition', window.scrollY);
|
||||
}
|
||||
|
||||
window.onload = function() {
|
||||
const scrollPosition = sessionStorage.getItem('scrollPosition');
|
||||
if (scrollPosition) {
|
||||
window.scrollTo(0, scrollPosition);
|
||||
sessionStorage.removeItem('scrollPosition');
|
||||
}
|
||||
}
|
||||
|
||||
function duplicateGroup(groupName) {
|
||||
fetch('/duplicate_group', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: `groupname=${groupName}`
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const newGroupName = 'Copy of ' + groupName;
|
||||
let newTable = `<table border="1">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Group Name</th>
|
||||
<th>Attributes</th>
|
||||
<th>Op</th>
|
||||
<th>Value</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<input type="text" id="new-groupname" value="${newGroupName}">
|
||||
</td>
|
||||
<td colspan="3" class="merged-cell"></td>
|
||||
<td></td>
|
||||
</tr>`;
|
||||
|
||||
data.forEach((attribute, index) => {
|
||||
newTable += `<tr>
|
||||
<td class="merged-cell"></td>
|
||||
<td><input type="text" class="new-attribute" value="${attribute.attribute}"></td>
|
||||
<td>
|
||||
<select class="new-op">
|
||||
<option value="=" ${attribute.op === '=' ? 'selected' : ''}>=</option>
|
||||
<option value="!=" ${attribute.op === '!=' ? 'selected' : ''}>!=</option>
|
||||
<option value=">" ${attribute.op === '>' ? 'selected' : ''}>></option>
|
||||
<option value="<" ${attribute.op === '<' ? 'selected' : ''}><</option>
|
||||
<option value=">=" ${attribute.op === '>=' ? 'selected' : ''}>>=</option>
|
||||
<option value="<=" ${attribute.op === '<=' ? 'selected' : ''}><=</option>
|
||||
</select>
|
||||
</td>
|
||||
<td><input type="text" class="new-value" value="${attribute.value}"></td>
|
||||
<td><button onclick="removeDuplicatedRow(this)">🗑️</button></td>
|
||||
</tr>`;
|
||||
});
|
||||
|
||||
newTable += `<tr>
|
||||
<td class="merged-cell"></td>
|
||||
<td colspan="3">
|
||||
<button onclick="addDuplicatedRow()">➕</button>
|
||||
</td>
|
||||
<td></td>
|
||||
</tr></tbody></table>`;
|
||||
|
||||
document.getElementById('duplicate-dialog-content').innerHTML = newTable;
|
||||
document.getElementById('duplicate-dialog').showModal();
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('close-dialog').addEventListener('click', () => {
|
||||
document.getElementById('duplicate-dialog').close();
|
||||
});
|
||||
|
||||
document.getElementById('save-duplicated-group').addEventListener('click', () => {
|
||||
saveDuplicatedGroup();
|
||||
});
|
||||
|
||||
function saveDuplicatedGroup() {
|
||||
let rows = document.querySelectorAll('#duplicate-dialog-content table tbody tr');
|
||||
let groupname = rows[0].querySelector('#new-groupname').value;
|
||||
let attributes = [];
|
||||
for (let i = 1; i < rows.length - 1; i++) {
|
||||
const attributeInput = rows[i].querySelector(`.new-attribute`);
|
||||
const opInput = rows[i].querySelector(`.new-op`);
|
||||
const valueInput = rows[i].querySelector(`.new-value`);
|
||||
|
||||
if (attributeInput && opInput && valueInput) {
|
||||
attributes.push({
|
||||
attribute: attributeInput.value,
|
||||
op: opInput.value,
|
||||
value: valueInput.value
|
||||
});
|
||||
} else {
|
||||
console.warn(`Input elements not found for row ${i}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
fetch('/save_duplicated_group', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ groupname: groupname, attributes: attributes })
|
||||
})
|
||||
.then(response => response.text())
|
||||
.then(data => {
|
||||
if (data === 'success') {
|
||||
document.getElementById('duplicate-dialog').close();
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Error saving duplicated group: ' + data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function addDuplicatedRow() {
|
||||
const table = document.querySelector('#duplicate-dialog-content table tbody');
|
||||
const newRow = table.insertRow(table.rows.length - 1);
|
||||
|
||||
const cell1 = newRow.insertCell(0);
|
||||
const cell2 = newRow.insertCell(1);
|
||||
const cell3 = newRow.insertCell(2);
|
||||
const cell4 = newRow.insertCell(3);
|
||||
const cell5 = newRow.insertCell(4);
|
||||
|
||||
cell1.classList.add('merged-cell');
|
||||
cell2.innerHTML = `<input type="text" class="new-attribute" value="">`;
|
||||
cell3.innerHTML = `
|
||||
<select class="new-op">
|
||||
<option value="=">=</option>
|
||||
<option value="!=">!=</option>
|
||||
<option value=">">></option>
|
||||
<option value="<"><</option>
|
||||
<option value=">=">>=</option>
|
||||
<option value="<="><=</option>
|
||||
</select>
|
||||
`;
|
||||
cell4.innerHTML = `<input type="text" class="new-value" value="">`;
|
||||
cell5.innerHTML = `<button onclick="removeDuplicatedRow(this)">🗑️</button>`;
|
||||
}
|
||||
|
||||
function removeDuplicatedRow(button) {
|
||||
const row = button.parentNode.parentNode;
|
||||
row.parentNode.removeChild(row);
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
window.onload = function () {
|
||||
const scrollPosition = sessionStorage.getItem("scrollPosition");
|
||||
if (scrollPosition) {
|
||||
window.scrollTo(0, parseInt(scrollPosition) - 100);
|
||||
sessionStorage.removeItem("scrollPosition");
|
||||
}
|
||||
};
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,43 +1,125 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}FreeRADIUS Manager{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>FreeRADIUS Manager</h1>
|
||||
<h1 class="page-title">FreeRADIUS Manager</h1>
|
||||
|
||||
<h2>Statistics:</h2>
|
||||
<p>Total Users: {{ total_users }}</p>
|
||||
<p>Total Groups: {{ total_groups }}</p>
|
||||
<div class="stats-cards">
|
||||
<div class="card neutral">
|
||||
<strong>Total Users</strong>
|
||||
<p>{{ total_users }}</p>
|
||||
</div>
|
||||
<div class="card neutral">
|
||||
<strong>Total Groups</strong>
|
||||
<p>{{ total_groups }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>SQL Query Tool:</h2>
|
||||
<form method="POST" action="/sql">
|
||||
<textarea name="query" rows="5" cols="50"></textarea><br>
|
||||
<button type="submit">Execute Query</button>
|
||||
</form>
|
||||
<h2>Recent Access Accepts</h2>
|
||||
<ul class="event-list green">
|
||||
{% for entry in latest_accept %}
|
||||
<li>
|
||||
<strong>{{ entry.username }}</strong>
|
||||
{% if entry.description %} ({{ entry.description }}){% endif %}
|
||||
— {{ entry.ago }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
{% if sql_results %}
|
||||
<h2>Query Results:</h2>
|
||||
<table border="1">
|
||||
<thead>
|
||||
<tr>
|
||||
{% for key in sql_results[0].keys() %}
|
||||
<th>{{ key }}</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in sql_results %}
|
||||
<tr>
|
||||
{% for value in row.values() %}
|
||||
<td>{{ value }}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
<h2>Recent Access Rejects</h2>
|
||||
<ul class="event-list red">
|
||||
{% for entry in latest_reject %}
|
||||
<li>
|
||||
<strong>{{ entry.username }}</strong>
|
||||
{% if entry.description %} ({{ entry.description }}){% endif %}
|
||||
— {{ entry.ago }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
{% if sql_error %}
|
||||
<p style="color: red;">{{ sql_error }}</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
<hr>
|
||||
|
||||
<h2>MAC Vendor Lookup</h2>
|
||||
<form id="mac-lookup-form" method="POST" action="/lookup_mac">
|
||||
<input type="text" name="mac" id="mac-input" placeholder="Enter MAC address" required>
|
||||
<button type="submit">🔍 Lookup</button>
|
||||
</form>
|
||||
|
||||
<pre id="mac-result" class="debug-output" style="margin-top: 1em;"></pre>
|
||||
|
||||
<script>
|
||||
document.getElementById('mac-lookup-form').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
const form = e.target;
|
||||
const data = new URLSearchParams(new FormData(form));
|
||||
const resultBox = document.getElementById('mac-result');
|
||||
resultBox.textContent = "Querying...";
|
||||
|
||||
fetch(form.action, {
|
||||
method: 'POST',
|
||||
body: data,
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
resultBox.textContent = JSON.stringify(data, null, 2);
|
||||
})
|
||||
.catch(err => {
|
||||
resultBox.textContent = `Error: ${err}`;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.page-title {
|
||||
margin-bottom: 1rem;
|
||||
color: var(--fg);
|
||||
}
|
||||
|
||||
.stats-cards {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.card {
|
||||
background: var(--cell-bg);
|
||||
border: 1px solid #666;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
.card.neutral {
|
||||
background-color: #444;
|
||||
}
|
||||
.event-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
.event-list li {
|
||||
padding: 4px 0;
|
||||
border-bottom: 1px dashed #666;
|
||||
}
|
||||
.event-list.green li { color: #4caf50; }
|
||||
.event-list.red li { color: #ff4d4d; }
|
||||
|
||||
#mac-lookup-form input {
|
||||
padding: 6px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #999;
|
||||
width: 250px;
|
||||
}
|
||||
#mac-lookup-form button {
|
||||
padding: 6px 12px;
|
||||
margin-left: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.debug-output {
|
||||
background-color: #222;
|
||||
color: #b6fcd5;
|
||||
border: 1px solid #333;
|
||||
padding: 1em;
|
||||
font-size: 0.9rem;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
100
app/templates/stats.html
Normal file
100
app/templates/stats.html
Normal file
@@ -0,0 +1,100 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}Authentication Stats{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="page-title">Authentication Stats</h1>
|
||||
|
||||
<div class="stats-container">
|
||||
<div class="card success-card">
|
||||
<h2>Last Access-Accept Events</h2>
|
||||
<table class="styled-table small-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>MAC Address</th>
|
||||
<th>Description</th>
|
||||
<th>Vendor</th>
|
||||
<th>Time</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for entry in accept_entries %}
|
||||
<tr>
|
||||
<td>{{ entry.username }}</td>
|
||||
<td>{{ entry.description or '' }}</td>
|
||||
<td>{{ entry.vendor }}</td>
|
||||
<td>{{ entry.ago }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="card error-card">
|
||||
<h2>Last Access-Reject Events</h2>
|
||||
<table class="styled-table small-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>MAC Address</th>
|
||||
<th>Description</th>
|
||||
<th>Vendor</th>
|
||||
<th>Time</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for entry in reject_entries %}
|
||||
<tr>
|
||||
<td>{{ entry.username }}</td>
|
||||
<td>{{ entry.description or '' }}</td>
|
||||
<td>{{ entry.vendor }}</td>
|
||||
<td>{{ entry.ago }}</td>
|
||||
<td>
|
||||
{% if entry.already_exists %}
|
||||
<span style="color: limegreen;">Already exists in {{ entry.existing_vlan or 'unknown VLAN' }}</span>
|
||||
{% else %}
|
||||
<form method="POST" action="/user/add_from_reject" style="display: flex; gap: 4px;">
|
||||
<input type="hidden" name="username" value="{{ entry.username }}">
|
||||
<select name="groupname" required>
|
||||
<option value="">Select VLAN</option>
|
||||
{% for group in available_groups %}
|
||||
<option value="{{ group }}">{{ group }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button type="submit" title="Add User">💾</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.stats-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 2rem;
|
||||
}
|
||||
.card {
|
||||
flex: 1;
|
||||
min-width: 45%;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
background-color: var(--card-bg);
|
||||
color: var(--fg);
|
||||
box-shadow: 0 0 10px rgba(0,0,0,0.2);
|
||||
}
|
||||
.success-card {
|
||||
border-left: 6px solid limegreen;
|
||||
}
|
||||
.error-card {
|
||||
border-left: 6px solid crimson;
|
||||
}
|
||||
.styled-table.small-table td, .styled-table.small-table th {
|
||||
padding: 6px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
@@ -1,364 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}User List{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>User List</h1>
|
||||
|
||||
{% for username, attributes in grouped_users.items() %}
|
||||
<table border="1">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>User Name</th>
|
||||
<th>Attributes</th>
|
||||
<th>Op</th>
|
||||
<th>Value</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<input type="text" id="username-{{ username }}" value="{{ username }}">
|
||||
</td>
|
||||
<td colspan="3" class="merged-cell">
|
||||
<button onclick="addUserRow('{{ username }}')">➕</button>
|
||||
</td>
|
||||
<td>
|
||||
<button onclick="updateUserName('{{ username }}')">✅ Rename User</button>
|
||||
<button onclick="location.reload()">❌</button>
|
||||
<a href="/delete_user_rows/{{ username }}" onclick="saveScrollPosition()">🗑️</a>
|
||||
<button onclick="duplicateUser('{{ username }}')">Duplicate</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% for attribute in attributes %}
|
||||
<tr>
|
||||
<td class="merged-cell"></td>
|
||||
<td><input type="text" id="attribute-{{ attribute.id }}" value="{{ attribute.attribute }}"></td>
|
||||
<td>
|
||||
<select id="op-{{ attribute.id }}">
|
||||
<option value="=" ${attribute.op === '=' ? 'selected' : ''}>=</option>
|
||||
<option value="!=" ${attribute.op === '!=' ? 'selected' : ''}>!=</option>
|
||||
<option value=">" ${attribute.op === '>' ? 'selected' : ''}>></option>
|
||||
<option value="<" ${attribute.op === '<' ? 'selected' : ''}><</option>
|
||||
<option value=">=" ${attribute.op === '>=' ? 'selected' : ''}>>=</option>
|
||||
<option value="<=" ${attribute.op === '<=' ? 'selected' : ''}><=</option>
|
||||
</select>
|
||||
</td>
|
||||
<td><input type="text" id="value-{{ attribute.id }}" value="{{ attribute.value }}"></td>
|
||||
<td>
|
||||
<button onclick="updateAttribute('{{ attribute.id }}')">✅</button>
|
||||
<button onclick="location.reload()">❌</button>
|
||||
<a href="/delete_user/{{ attribute.id }}" onclick="saveScrollPosition()">🗑️</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endfor %}
|
||||
|
||||
<table border="1">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>User Name</th>
|
||||
<th>Attributes</th>
|
||||
<th>Op</th>
|
||||
<th>Value</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<input type="text" id="new-username" value="">
|
||||
</td>
|
||||
<td colspan="3" class="merged-cell"></td>
|
||||
<td>
|
||||
<button onclick="addNewUser()">Add New User</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<dialog id="duplicate-dialog">
|
||||
<div id="duplicate-dialog-content"></div>
|
||||
<button id="close-dialog">❌</button>
|
||||
<button id="save-duplicated-user">Save</button>
|
||||
</dialog>
|
||||
|
||||
<style>
|
||||
.merged-cell {
|
||||
border: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
function updateAttribute(attributeId) {
|
||||
const attribute = document.getElementById(`attribute-${attributeId}`).value;
|
||||
const op = document.getElementById(`op-${attributeId}`).value;
|
||||
const value = document.getElementById(`value-${attributeId}`).value;
|
||||
|
||||
fetch('/update_user_attribute', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: `attributeId=${attributeId}&attribute=${attribute}&op=${op}&value=${value}`
|
||||
})
|
||||
.then(response => response.text())
|
||||
.then(data => {
|
||||
if (data === 'success') {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Error updating attribute: ' + data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateUserName(oldUserName) {
|
||||
const newUserName = document.getElementById(`username-${oldUserName}`).value;
|
||||
|
||||
fetch('/update_user_name', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: `oldUserName=${oldUserName}&newUserName=${newUserName}`
|
||||
})
|
||||
.then(response => response.text())
|
||||
.then(data => {
|
||||
if (data === 'success') {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Error updating user name: ' + data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function addUserRow(userName) {
|
||||
const table = event.target.closest('table').querySelector('tbody');
|
||||
const newRow = table.insertRow(table.rows.length);
|
||||
|
||||
const cell1 = newRow.insertCell(0);
|
||||
const cell2 = newRow.insertCell(1);
|
||||
const cell3 = newRow.insertCell(2);
|
||||
const cell4 = newRow.insertCell(3);
|
||||
const cell5 = newRow.insertCell(4);
|
||||
|
||||
cell1.classList.add('merged-cell');
|
||||
cell2.innerHTML = '<input type="text" id="new-attribute" value="">';
|
||||
cell3.innerHTML = `
|
||||
<select id="new-op">
|
||||
<option value="=">=</option>
|
||||
<option value="!=">!=</option>
|
||||
<option value=">">></option>
|
||||
<option value="<"><</option>
|
||||
<option value=">=">>=</option>
|
||||
<option value="<="><=</option>
|
||||
</select>
|
||||
`;
|
||||
cell4.innerHTML = '<input type="text" id="new-value" value="">';
|
||||
cell5.innerHTML = '<button onclick="saveNewUserRow(\'' + userName + '\', this)">✅</button> <button onclick="removeUserRow(this)">❌</button>';
|
||||
}
|
||||
|
||||
function saveNewUserRow(userName, button) {
|
||||
const row = button.parentNode.parentNode;
|
||||
const attribute = row.querySelector('#new-attribute').value;
|
||||
const op = row.querySelector('#new-op').value;
|
||||
const value = row.querySelector('#new-value').value;
|
||||
|
||||
fetch('/add_user_attribute', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: `username=${userName}&attribute=${attribute}&op=${op}&value=${value}`
|
||||
})
|
||||
.then(response => response.text())
|
||||
.then(data => {
|
||||
if (data === 'success') {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Error adding attribute: ' + data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function removeUserRow(button) {
|
||||
const row = button.parentNode.parentNode;
|
||||
row.parentNode.removeChild(row);
|
||||
}
|
||||
|
||||
function addNewUser() {
|
||||
const newUserName = document.getElementById('new-username').value;
|
||||
|
||||
fetch('/add_user', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: `username=${newUserName}`
|
||||
})
|
||||
.then(response => response.text())
|
||||
.then(data => {
|
||||
if (data === 'success') {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Error adding user: ' + data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function saveScrollPosition() {
|
||||
sessionStorage.setItem('scrollPosition', window.scrollY);
|
||||
}
|
||||
|
||||
window.onload = function() {
|
||||
const scrollPosition = sessionStorage.getItem('scrollPosition');
|
||||
if (scrollPosition) {
|
||||
window.scrollTo(0, scrollPosition);
|
||||
sessionStorage.removeItem('scrollPosition');
|
||||
}
|
||||
}
|
||||
|
||||
function duplicateUser(userName) {
|
||||
fetch('/duplicate_user', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: `username=${userName}`
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const newUserName = 'Copy of ' + userName;
|
||||
let newTable = `<table border="1">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>User Name</th>
|
||||
<th>Attributes</th>
|
||||
<th>Op</th>
|
||||
<th>Value</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<input type="text" id="new-username" value="${newUserName}">
|
||||
</td>
|
||||
<td colspan="3" class="merged-cell"></td>
|
||||
<td></td>
|
||||
</tr>`;
|
||||
|
||||
data.forEach((attribute, index) => {
|
||||
newTable += `<tr>
|
||||
<td class="merged-cell"></td>
|
||||
<td><input type="text" class="new-attribute" value="${attribute.attribute}"></td>
|
||||
<td>
|
||||
<select class="new-op">
|
||||
<option value="=" ${attribute.op === '=' ? 'selected' : ''}>=</option>
|
||||
<option value="!=" ${attribute.op === '!=' ? 'selected' : ''}>!=</option>
|
||||
<option value=">" ${attribute.op === '>' ? 'selected' : ''}>></option>
|
||||
<option value="<" ${attribute.op === '<' ? 'selected' : ''}><</option>
|
||||
<option value=">=" ${attribute.op === '>=' ? 'selected' : ''}>>=</option>
|
||||
<option value="<=" ${attribute.op === '<=' ? 'selected' : ''}><=</option>
|
||||
</select>
|
||||
</td>
|
||||
<td><input type="text" class="new-value" value="${attribute.value}"></td>
|
||||
<td><button onclick="removeDuplicatedUserRow(this)">🗑️</button></td>
|
||||
</tr>`;
|
||||
});
|
||||
|
||||
newTable += `<tr>
|
||||
<td class="merged-cell"></td>
|
||||
<td colspan="3">
|
||||
<button onclick="addDuplicatedUserRow()">➕</button>
|
||||
</td>
|
||||
<td></td>
|
||||
</tr></tbody></table>`;
|
||||
|
||||
document.getElementById('duplicate-dialog-content').innerHTML = newTable;
|
||||
document.getElementById('duplicate-dialog').showModal();
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('close-dialog').addEventListener('click', () => {
|
||||
document.getElementById('duplicate-dialog').close();
|
||||
});
|
||||
|
||||
document.getElementById('save-duplicated-user').addEventListener('click', () => {
|
||||
saveDuplicatedUser();
|
||||
});
|
||||
|
||||
function saveDuplicatedUser() {
|
||||
let rows = document.querySelectorAll('#duplicate-dialog-content table tbody tr');
|
||||
let username = rows[0].querySelector('#new-username').value;
|
||||
let attributes = [];
|
||||
for (let i = 1; i < rows.length - 1; i++) {
|
||||
const attributeInput = rows[i].querySelector(`.new-attribute`);
|
||||
const opInput = rows[i].querySelector(`.new-op`);
|
||||
const valueInput = rows[i].querySelector(`.new-value`);
|
||||
|
||||
if (attributeInput && opInput && valueInput) {
|
||||
attributes.push({
|
||||
attribute: attributeInput.value,
|
||||
op: opInput.value,
|
||||
value: valueInput.value
|
||||
});
|
||||
} else {
|
||||
console.warn(`Input elements not found for row ${i}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
fetch('/save_duplicated_user', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ username: username, attributes: attributes })
|
||||
})
|
||||
.then(response => response.text())
|
||||
.then(data => {
|
||||
if (data === 'success') {
|
||||
document.getElementById('duplicate-dialog').close();
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Error saving duplicated user: ' + data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function addDuplicatedUserRow() {
|
||||
const table = document.querySelector('#duplicate-dialog-content table tbody');
|
||||
const newRow = table.insertRow(table.rows.length - 1);
|
||||
|
||||
const cell1 = newRow.insertCell(0);
|
||||
const cell2 = newRow.insertCell(1);
|
||||
const cell3 = newRow.insertCell(2);
|
||||
const cell4 = newRow.insertCell(3);
|
||||
const cell5 = newRow.insertCell(4);
|
||||
|
||||
cell1.classList.add('merged-cell');
|
||||
cell2.innerHTML = `<input type="text" class="new-attribute" value="">`;
|
||||
cell3.innerHTML = `
|
||||
<select class="new-op">
|
||||
<option value="=">=</option>
|
||||
<option value="!=">!=</option>
|
||||
<option value=">">></option>
|
||||
<option value="<"><</option>
|
||||
<option value=">=">>=</option>
|
||||
<option value="<="><=</option>
|
||||
</select>
|
||||
`;
|
||||
cell4.innerHTML = `<input type="text" class="new-value" value="">`;
|
||||
cell5.innerHTML = `<button onclick="removeDuplicatedUserRow(this)">🗑️</button>`;
|
||||
}
|
||||
|
||||
function removeDuplicatedUserRow(button) {
|
||||
const row = button.parentNode.parentNode;
|
||||
row.parentNode.removeChild(row);
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,262 +1,179 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}User List{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>User List</h1>
|
||||
<h1 class="page-title">User List</h1>
|
||||
|
||||
<table border="1">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>MAC Address</th>
|
||||
<th>Description</th>
|
||||
<th>VLAN ID</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for user in results %}
|
||||
<tr>
|
||||
<td>
|
||||
<input type="text" id="mac_address-{{ user.mac_address }}" value="{{ user.mac_address }}" maxlength="12" pattern="^[0-9a-fA-F]{12}$">
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" id="description-{{ user.mac_address }}" value="{{ user.description }}" maxlength="200">
|
||||
</td>
|
||||
<td>
|
||||
<select id="vlan_id-{{ user.mac_address }}">
|
||||
{% for group in groups %}
|
||||
<option value="{{ group.groupname }}" {% if user.vlan_id == group.groupname %} selected {% endif %}>
|
||||
{{ group.groupname }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<button onclick="updateUser('{{ user.mac_address }}')">✅</button>
|
||||
<a href="/delete_user/{{ user.mac_address }}" onclick="saveScrollPosition()">🗑️</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<tr>
|
||||
<td colspan="4">
|
||||
<button onclick="addNewUserRow()">➕ Add User</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table class="styled-table fade-in">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>MAC Address</th>
|
||||
<th>
|
||||
Vendor
|
||||
<button class="icon-button" onclick="refreshVendors(this)" title="Refresh Vendor">🔄</button>
|
||||
</th>
|
||||
<th>Description</th>
|
||||
<th>Group</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="user-body">
|
||||
<!-- New User Row -->
|
||||
<tr class="new-row">
|
||||
<td><input type="text" id="new-mac" placeholder="MAC address"></td>
|
||||
<td><em>(auto)</em></td>
|
||||
<td><input type="text" id="new-description" placeholder="Description"></td>
|
||||
<td>
|
||||
<select id="new-vlan">
|
||||
<option value="">-- Select Group --</option>
|
||||
{% for group in groups %}
|
||||
<option value="{{ group.groupname }}">{{ group.groupname }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<button class="icon-button pulse" onclick="addUser()" title="Save User">💾</button>
|
||||
<button class="icon-button" onclick="clearUserFields()" title="Reset">❌</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<dialog id="add-user-dialog">
|
||||
<div id="add-user-dialog-content">
|
||||
<table border="1">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>MAC Address</th>
|
||||
<th>Description</th>
|
||||
<th>VLAN ID</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><input type="text" id="new-mac" maxlength="12" pattern="^[0-9a-fA-F]{12}$"></td>
|
||||
<td><input type="text" id="new-description" maxlength="200"></td>
|
||||
<td>
|
||||
<select id="new-vlan_id">
|
||||
{% for group in groups %}
|
||||
<option value="{{ group.groupname }}">
|
||||
{{ group.groupname }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: flex-end; margin-top: 10px;">
|
||||
<button id="cancel-add-user-dialog">Cancel</button>
|
||||
<button id="save-new-user">Save</button>
|
||||
</div>
|
||||
</dialog>
|
||||
{% for row in results %}
|
||||
<tr>
|
||||
<td><input type="text" value="{{ row.mac_address }}" id="mac-{{ loop.index }}" disabled></td>
|
||||
<td>{{ row.vendor or 'Unknown Vendor' }}</td>
|
||||
<td><input type="text" value="{{ row.description }}" id="desc-{{ loop.index }}"></td>
|
||||
<td>
|
||||
<select id="vlan-{{ loop.index }}">
|
||||
{% for group in groups %}
|
||||
<option value="{{ group.groupname }}" {% if group.groupname == row.vlan_id %}selected{% endif %}>
|
||||
{{ group.groupname }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<button class="icon-button" onclick="enableUserEdit({{ loop.index }})" title="Edit">✏️</button>
|
||||
<button class="icon-button" onclick="updateUser({{ loop.index }}, '{{ row.mac_address }}')" title="Save">💾</button>
|
||||
<button class="icon-button" onclick="location.reload()" title="Cancel">❌</button>
|
||||
<a class="icon-button" href="/user/delete_user/{{ row.mac_address }}" onclick="saveScrollPosition()" title="Delete">🗑️</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<style>
|
||||
#cancel-add-user-dialog {
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
background-color: #f44336;
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
margin-right: 10px;
|
||||
<script>
|
||||
function enableUserEdit(index) {
|
||||
const input = document.getElementById(`mac-${index}`);
|
||||
input.disabled = false;
|
||||
input.focus();
|
||||
}
|
||||
|
||||
function clearUserFields() {
|
||||
document.getElementById("new-mac").value = "";
|
||||
document.getElementById("new-description").value = "";
|
||||
document.getElementById("new-vlan").selectedIndex = 0;
|
||||
}
|
||||
|
||||
function addUser() {
|
||||
const mac = document.getElementById("new-mac").value;
|
||||
const desc = document.getElementById("new-description").value;
|
||||
const vlan = document.getElementById("new-vlan").value;
|
||||
|
||||
if (!mac || !vlan) {
|
||||
showToast("MAC address and group are required.");
|
||||
return;
|
||||
}
|
||||
|
||||
fetch("/user/add_user", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ mac_address: mac, description: desc, vlan_id: vlan })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showToast("User added.");
|
||||
setTimeout(() => location.reload(), 800);
|
||||
} else {
|
||||
showToast("Error: " + data.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateUser(index, originalMac) {
|
||||
const macInput = document.getElementById(`mac-${index}`);
|
||||
const desc = document.getElementById(`desc-${index}`).value;
|
||||
const vlan = document.getElementById(`vlan-${index}`).value;
|
||||
|
||||
fetch("/user/update_user", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: `mac_address=${originalMac}&new_mac_address=${macInput.value}&description=${desc}&vlan_id=${vlan}`
|
||||
})
|
||||
.then(res => res.text())
|
||||
.then(data => {
|
||||
if (data === "success") {
|
||||
showToast("User updated.");
|
||||
setTimeout(() => location.reload(), 800);
|
||||
} else {
|
||||
showToast("Update failed: " + data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// function refreshVendors() {
|
||||
// showToast("Refreshing vendor info...");
|
||||
// fetch("/user/refresh_vendors", {
|
||||
// method: "POST"
|
||||
// })
|
||||
// .then(res => res.json())
|
||||
// .then(data => {
|
||||
// showToast(data.message || "Refreshed.");
|
||||
// setTimeout(() => location.reload(), 1200);
|
||||
// })
|
||||
// .catch(() => showToast("Failed to refresh vendor info."));
|
||||
// }
|
||||
|
||||
function refreshVendors(btn) {
|
||||
btn.disabled = true;
|
||||
showToast("Refreshing vendor info...");
|
||||
|
||||
function refreshCycle() {
|
||||
fetch("/user/refresh_vendors", { method: "POST" })
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showToast(`Updated ${data.updated} vendors`);
|
||||
if (data.remaining) {
|
||||
setTimeout(refreshCycle, 1500); // Pause before next batch
|
||||
} else {
|
||||
showToast("Vendor refresh complete.");
|
||||
setTimeout(() => location.reload(), 1000);
|
||||
}
|
||||
} else {
|
||||
showToast("Refresh failed: " + data.message);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
showToast("Error during vendor refresh.");
|
||||
});
|
||||
}
|
||||
|
||||
#cancel-add-user-dialog:hover {
|
||||
background-color: #d32f2f;
|
||||
}
|
||||
refreshCycle();
|
||||
}
|
||||
|
||||
#save-new-user {
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
function saveScrollPosition() {
|
||||
sessionStorage.setItem("scrollPosition", window.scrollY);
|
||||
}
|
||||
|
||||
#save-new-user:hover {
|
||||
background-color: #45a049;
|
||||
}
|
||||
|
||||
#add-user-dialog-content + div {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// Data from Flask (VLAN groups)
|
||||
const groups = {{ groups | tojson | safe }};
|
||||
|
||||
/**
|
||||
* Updates a user's MAC address, description, and VLAN ID.
|
||||
* @param {string} mac_address - The original MAC address of the user.
|
||||
*/
|
||||
function updateUser(mac_address) {
|
||||
const descriptionInput = document.getElementById('description-' + mac_address);
|
||||
const macInput = document.getElementById('mac_address-' + mac_address);
|
||||
const vlan_id = document.getElementById('vlan_id-' + mac_address).value;
|
||||
const new_mac_address = macInput.value;
|
||||
const description = descriptionInput.value;
|
||||
|
||||
// Client-side validation for MAC address
|
||||
if (new_mac_address.length !== 12 || !/^[0-9a-fA-F]{12}$/.test(new_mac_address)) {
|
||||
alert("MAC Address must be 12 hexadecimal characters.");
|
||||
macInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// Client-side validation for description
|
||||
if (description.length > 200) {
|
||||
alert("Description must be 200 characters or less.");
|
||||
descriptionInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Updating user:", mac_address, description, vlan_id, new_mac_address);
|
||||
|
||||
// Send update request to server
|
||||
fetch('/update_user', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: `mac_address=${mac_address}&description=${description}&vlan_id=${vlan_id}&new_mac_address=${new_mac_address}`
|
||||
})
|
||||
.then(response => response.text())
|
||||
.then(data => {
|
||||
console.log("Server response:", data);
|
||||
if (data === 'success') {
|
||||
// Update UI on success
|
||||
macInput.value = new_mac_address;
|
||||
descriptionInput.value = description;
|
||||
document.getElementById('vlan_id-' + mac_address).value = vlan_id;
|
||||
macInput.id = 'mac_address-' + new_mac_address;
|
||||
descriptionInput.id = 'description-' + new_mac_address;
|
||||
document.getElementById('vlan_id-' + mac_address).id = 'vlan_id-' + new_mac_address;
|
||||
} else {
|
||||
alert('Error updating user: ' + data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the current scroll position in session storage.
|
||||
*/
|
||||
function saveScrollPosition() {
|
||||
sessionStorage.setItem('scrollPosition', window.scrollY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores the scroll position from session storage on page load.
|
||||
*/
|
||||
window.onload = function () {
|
||||
const scrollPosition = sessionStorage.getItem('scrollPosition');
|
||||
if (scrollPosition) {
|
||||
window.scrollTo(0, scrollPosition);
|
||||
sessionStorage.removeItem('scrollPosition');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the "Add User" dialog.
|
||||
*/
|
||||
function addNewUserRow() {
|
||||
document.getElementById('add-user-dialog').showModal();
|
||||
}
|
||||
|
||||
// Close dialog on cancel button click
|
||||
document.getElementById('cancel-add-user-dialog').addEventListener('click', () => {
|
||||
document.getElementById('add-user-dialog').close();
|
||||
});
|
||||
|
||||
// Save new user on save button click
|
||||
document.getElementById('save-new-user').addEventListener('click', () => {
|
||||
saveNewUser();
|
||||
});
|
||||
|
||||
/**
|
||||
* Saves a new user to the database.
|
||||
*/
|
||||
function saveNewUser() {
|
||||
const macInput = document.getElementById('new-mac');
|
||||
const descriptionInput = document.getElementById('new-description');
|
||||
const vlan_id = document.getElementById('new-vlan_id').value;
|
||||
const mac = macInput.value;
|
||||
const description = descriptionInput.value;
|
||||
|
||||
// Client-side validation for MAC address
|
||||
if (mac.length !== 12 || !/^[0-9a-fA-F]{12}$/.test(mac)) {
|
||||
alert("MAC Address must be 12 hexadecimal characters.");
|
||||
macInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// Client-side validation for description
|
||||
if (description.length > 200) {
|
||||
alert("Description must be 200 characters or less.");
|
||||
descriptionInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// Send add user request to server
|
||||
fetch('/add_user', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ mac_address: mac, description: description, vlan_id: vlan_id }),
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
return response.text().then(text => {
|
||||
throw new Error(`HTTP error! status: ${response.status}, body: ${text}`);
|
||||
});
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data && data.success) {
|
||||
document.getElementById('add-user-dialog').close();
|
||||
location.reload(); // Refresh the page to show the new user
|
||||
} else {
|
||||
alert('Error adding user: ' + (data && data.message ? data.message : 'Unknown error'));
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Fetch error:', error);
|
||||
alert('Error adding user: ' + error.message);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
window.onload = function () {
|
||||
const scroll = sessionStorage.getItem("scrollPosition");
|
||||
if (scroll) {
|
||||
window.scrollTo(0, parseInt(scroll) - 100);
|
||||
sessionStorage.removeItem("scrollPosition");
|
||||
}
|
||||
};
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
3
app/views/__init__.py
Normal file
3
app/views/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .index_views import index # not index_views
|
||||
from .user_views import user
|
||||
from .group_views import group
|
||||
BIN
app/views/__pycache__/__init__.cpython-39.pyc
Normal file
BIN
app/views/__pycache__/__init__.cpython-39.pyc
Normal file
Binary file not shown.
BIN
app/views/__pycache__/group_views.cpython-39.pyc
Normal file
BIN
app/views/__pycache__/group_views.cpython-39.pyc
Normal file
Binary file not shown.
BIN
app/views/__pycache__/index_views.cpython-39.pyc
Normal file
BIN
app/views/__pycache__/index_views.cpython-39.pyc
Normal file
Binary file not shown.
BIN
app/views/__pycache__/user_views.cpython-39.pyc
Normal file
BIN
app/views/__pycache__/user_views.cpython-39.pyc
Normal file
Binary file not shown.
201
app/views/group_views.py
Normal file
201
app/views/group_views.py
Normal file
@@ -0,0 +1,201 @@
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, jsonify
|
||||
from database import get_db
|
||||
import mysql.connector
|
||||
|
||||
group = Blueprint('group', __name__)
|
||||
|
||||
@group.route('/groups')
|
||||
def groups():
|
||||
db = get_db()
|
||||
if db:
|
||||
cursor = db.cursor()
|
||||
try:
|
||||
cursor.execute("SELECT DISTINCT groupname FROM radgroupcheck")
|
||||
group_names = [row[0] for row in cursor.fetchall()]
|
||||
grouped_results = {}
|
||||
|
||||
for groupname in group_names:
|
||||
cursor.execute("SELECT id, attribute, op, value FROM radgroupreply WHERE groupname = %s", (groupname,))
|
||||
attributes = cursor.fetchall()
|
||||
grouped_results[groupname] = [
|
||||
{'id': row[0], 'attribute': row[1], 'op': row[2], 'value': row[3]}
|
||||
for row in attributes
|
||||
]
|
||||
|
||||
cursor.close()
|
||||
db.close()
|
||||
return render_template('group_list_nested.html', grouped_results=grouped_results)
|
||||
|
||||
except mysql.connector.Error as err:
|
||||
print(f"Database Error: {err}")
|
||||
cursor.close()
|
||||
db.close()
|
||||
return render_template('group_list_nested.html', grouped_results={})
|
||||
return "Database Connection Failed"
|
||||
|
||||
@group.route('/save_group', methods=['POST'])
|
||||
def save_group():
|
||||
data = request.get_json()
|
||||
groupname = data.get('groupname')
|
||||
attributes = data.get('attributes')
|
||||
|
||||
if not groupname or not attributes:
|
||||
return jsonify({'error': 'Group name and attributes are required'}), 400
|
||||
|
||||
db = get_db()
|
||||
cursor = db.cursor()
|
||||
|
||||
try:
|
||||
# Prevent duplicates
|
||||
cursor.execute("SELECT 1 FROM radgroupcheck WHERE groupname = %s", (groupname,))
|
||||
if cursor.fetchone():
|
||||
return jsonify({'error': f'Group name "{groupname}" already exists'}), 400
|
||||
|
||||
# Insert baseline group rule
|
||||
cursor.execute("""
|
||||
INSERT INTO radgroupcheck (groupname, attribute, op, value)
|
||||
VALUES (%s, 'Auth-Type', ':=', 'Accept')
|
||||
""", (groupname,))
|
||||
|
||||
# Insert attributes
|
||||
for attr in attributes:
|
||||
cursor.execute("""
|
||||
INSERT INTO radgroupreply (groupname, attribute, op, value)
|
||||
VALUES (%s, %s, %s, %s)
|
||||
""", (groupname, attr['attribute'], attr['op'], attr['value']))
|
||||
|
||||
db.commit()
|
||||
cursor.close()
|
||||
db.close()
|
||||
return jsonify({'success': True})
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
cursor.close()
|
||||
db.close()
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@group.route('/update_group_name', methods=['POST'])
|
||||
def update_group_name():
|
||||
old_groupname = request.form.get('oldGroupName')
|
||||
new_groupname = request.form.get('newGroupName')
|
||||
|
||||
if not old_groupname or not new_groupname:
|
||||
return jsonify({'error': 'Both old and new group names are required'}), 400
|
||||
|
||||
db = get_db()
|
||||
cursor = db.cursor()
|
||||
try:
|
||||
cursor.execute("UPDATE radgroupcheck SET groupname=%s WHERE groupname=%s", (new_groupname, old_groupname))
|
||||
cursor.execute("UPDATE radgroupreply SET groupname=%s WHERE groupname=%s", (new_groupname, old_groupname))
|
||||
cursor.execute("UPDATE radusergroup SET groupname=%s WHERE groupname=%s", (new_groupname, old_groupname))
|
||||
db.commit()
|
||||
cursor.close()
|
||||
db.close()
|
||||
return jsonify({'success': True}), 200
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
cursor.close()
|
||||
db.close()
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@group.route('/update_attribute', methods=['POST'])
|
||||
def update_attribute():
|
||||
attribute_id = request.form.get('attributeId')
|
||||
attribute = request.form.get('attribute')
|
||||
op = request.form.get('op')
|
||||
value = request.form.get('value')
|
||||
|
||||
if not attribute_id or not attribute or not op or not value:
|
||||
return jsonify({'error': 'All fields are required'}), 400
|
||||
|
||||
db = get_db()
|
||||
cursor = db.cursor()
|
||||
try:
|
||||
cursor.execute("""
|
||||
UPDATE radgroupreply
|
||||
SET attribute=%s, op=%s, value=%s
|
||||
WHERE id=%s
|
||||
""", (attribute, op, value, attribute_id))
|
||||
db.commit()
|
||||
cursor.close()
|
||||
db.close()
|
||||
return jsonify({'success': True}), 200
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
cursor.close()
|
||||
db.close()
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@group.route('/delete_group_rows/<groupname>')
|
||||
def delete_group_rows(groupname):
|
||||
db = get_db()
|
||||
if db:
|
||||
cursor = db.cursor()
|
||||
try:
|
||||
cursor.execute("DELETE FROM radgroupreply WHERE groupname = %s", (groupname,))
|
||||
cursor.execute("DELETE FROM radusergroup WHERE groupname = %s", (groupname,))
|
||||
cursor.execute("DELETE FROM radgroupcheck WHERE groupname = %s", (groupname,))
|
||||
db.commit()
|
||||
cursor.close()
|
||||
db.close()
|
||||
return redirect(url_for('group.groups'))
|
||||
except mysql.connector.Error as err:
|
||||
db.rollback()
|
||||
cursor.close()
|
||||
db.close()
|
||||
return redirect(url_for('group.groups'))
|
||||
return "Database Connection Failed"
|
||||
|
||||
@group.route('/delete_group/<int:group_id>')
|
||||
def delete_group(group_id):
|
||||
db = get_db()
|
||||
if db:
|
||||
cursor = db.cursor()
|
||||
try:
|
||||
cursor.execute("DELETE FROM radgroupreply WHERE id = %s", (group_id,))
|
||||
cursor.execute("DELETE FROM radgroupcheck WHERE id = %s", (group_id,))
|
||||
db.commit()
|
||||
cursor.close()
|
||||
db.close()
|
||||
return redirect(url_for('group.groups'))
|
||||
except mysql.connector.Error as err:
|
||||
db.rollback()
|
||||
cursor.close()
|
||||
db.close()
|
||||
return redirect(url_for('group.groups'))
|
||||
return "Database Connection Failed"
|
||||
|
||||
@group.route('/duplicate_group', methods=['POST'])
|
||||
def duplicate_group():
|
||||
groupname = request.form.get('groupname')
|
||||
if not groupname:
|
||||
return jsonify({'error': 'Group name is required'}), 400
|
||||
|
||||
db = get_db()
|
||||
cursor = db.cursor()
|
||||
|
||||
try:
|
||||
cursor.execute("SELECT attribute, op, value FROM radgroupreply WHERE groupname = %s", (groupname,))
|
||||
attributes = cursor.fetchall()
|
||||
if not attributes:
|
||||
return jsonify({'error': f'Group "{groupname}" not found or has no attributes'}), 404
|
||||
|
||||
new_groupname = f"Copy of {groupname}"
|
||||
count = 1
|
||||
while True:
|
||||
cursor.execute("SELECT 1 FROM radgroupcheck WHERE groupname = %s", (new_groupname,))
|
||||
if not cursor.fetchone():
|
||||
break
|
||||
count += 1
|
||||
new_groupname = f"Copy of {groupname} ({count})"
|
||||
|
||||
attr_list = [{'attribute': row[0], 'op': row[1], 'value': row[2]} for row in attributes]
|
||||
cursor.close()
|
||||
db.close()
|
||||
return jsonify({'new_groupname': new_groupname, 'attributes': attr_list})
|
||||
|
||||
except Exception as e:
|
||||
cursor.close()
|
||||
db.close()
|
||||
return jsonify({'error': str(e)}), 500
|
||||
187
app/views/index_views.py
Normal file
187
app/views/index_views.py
Normal file
@@ -0,0 +1,187 @@
|
||||
from flask import Blueprint, render_template, request, jsonify
|
||||
from database import get_db
|
||||
from datetime import datetime
|
||||
import requests
|
||||
|
||||
index = Blueprint('index', __name__)
|
||||
OUI_API_URL = 'https://api.maclookup.app/v2/macs/{}'
|
||||
|
||||
|
||||
def time_ago(dt):
|
||||
now = datetime.now()
|
||||
diff = now - dt
|
||||
seconds = int(diff.total_seconds())
|
||||
if seconds < 60:
|
||||
return f"{seconds}s ago"
|
||||
elif seconds < 3600:
|
||||
return f"{seconds//60}m{seconds%60}s ago"
|
||||
elif seconds < 86400:
|
||||
return f"{seconds//3600}h{(seconds%3600)//60}m ago"
|
||||
else:
|
||||
return f"{seconds//86400}d{(seconds%86400)//3600}h ago"
|
||||
|
||||
|
||||
def lookup_vendor(mac):
|
||||
prefix = mac.replace(":", "").replace("-", "").upper()[:6]
|
||||
db = get_db()
|
||||
cursor = db.cursor(dictionary=True)
|
||||
|
||||
# Try local DB first
|
||||
cursor.execute("SELECT vendor_name FROM mac_vendor_cache WHERE mac_prefix = %s", (prefix,))
|
||||
result = cursor.fetchone()
|
||||
|
||||
if result and result['vendor_name'] != "Unknown Vendor":
|
||||
return {"source": "local", "prefix": prefix, "vendor": result['vendor_name']}
|
||||
|
||||
# Try API fallback
|
||||
try:
|
||||
api_url = OUI_API_URL.format(mac)
|
||||
r = requests.get(api_url, timeout=3)
|
||||
if r.status_code == 200:
|
||||
data = r.json()
|
||||
vendor = data.get("company", "Unknown Vendor")
|
||||
|
||||
# Save to DB
|
||||
cursor.execute("""
|
||||
INSERT INTO mac_vendor_cache (mac_prefix, vendor_name, last_updated)
|
||||
VALUES (%s, %s, NOW())
|
||||
ON DUPLICATE KEY UPDATE vendor_name = VALUES(vendor_name), last_updated = NOW()
|
||||
""", (prefix, vendor))
|
||||
db.commit()
|
||||
return {"source": "api", "prefix": prefix, "vendor": vendor, "raw": data}
|
||||
else:
|
||||
return {"source": "api", "prefix": prefix, "error": f"API returned status {r.status_code}", "raw": r.text}
|
||||
except Exception as e:
|
||||
return {"source": "api", "prefix": prefix, "error": str(e)}
|
||||
finally:
|
||||
cursor.close()
|
||||
|
||||
|
||||
@index.route('/')
|
||||
def homepage():
|
||||
db = get_db()
|
||||
latest_accept = []
|
||||
latest_reject = []
|
||||
total_users = 0
|
||||
total_groups = 0
|
||||
|
||||
if db:
|
||||
cursor = db.cursor(dictionary=True)
|
||||
|
||||
cursor.execute("SELECT COUNT(*) AS count FROM radcheck")
|
||||
total_users = cursor.fetchone()['count']
|
||||
|
||||
cursor.execute("SELECT COUNT(DISTINCT groupname) AS count FROM radgroupcheck")
|
||||
total_groups = cursor.fetchone()['count']
|
||||
|
||||
cursor.execute("""
|
||||
SELECT p.username, d.description, p.reply, p.authdate
|
||||
FROM radpostauth p
|
||||
LEFT JOIN rad_description d ON p.username = d.username
|
||||
WHERE p.reply = 'Access-Accept'
|
||||
ORDER BY p.authdate DESC LIMIT 5
|
||||
""")
|
||||
latest_accept = cursor.fetchall()
|
||||
for row in latest_accept:
|
||||
row['ago'] = time_ago(row['authdate'])
|
||||
|
||||
cursor.execute("""
|
||||
SELECT p.username, d.description, p.reply, p.authdate
|
||||
FROM radpostauth p
|
||||
LEFT JOIN rad_description d ON p.username = d.username
|
||||
WHERE p.reply = 'Access-Reject'
|
||||
ORDER BY p.authdate DESC LIMIT 5
|
||||
""")
|
||||
latest_reject = cursor.fetchall()
|
||||
for row in latest_reject:
|
||||
row['ago'] = time_ago(row['authdate'])
|
||||
|
||||
cursor.close()
|
||||
db.close()
|
||||
|
||||
return render_template('index.html',
|
||||
total_users=total_users,
|
||||
total_groups=total_groups,
|
||||
latest_accept=latest_accept,
|
||||
latest_reject=latest_reject)
|
||||
|
||||
|
||||
@index.route('/stats')
|
||||
def stats():
|
||||
db = get_db()
|
||||
accept_entries = []
|
||||
reject_entries = []
|
||||
available_groups = []
|
||||
|
||||
if db:
|
||||
cursor = db.cursor(dictionary=True)
|
||||
|
||||
# Fetch available VLANs
|
||||
cursor.execute("SELECT DISTINCT groupname FROM radgroupcheck ORDER BY groupname")
|
||||
available_groups = [row['groupname'] for row in cursor.fetchall()]
|
||||
|
||||
# Get existing users and map to group
|
||||
cursor.execute("""
|
||||
SELECT r.username, g.groupname
|
||||
FROM radcheck r
|
||||
LEFT JOIN radusergroup g ON r.username = g.username
|
||||
""")
|
||||
existing_user_map = {
|
||||
row['username'].replace(":", "").replace("-", "").upper(): row['groupname']
|
||||
for row in cursor.fetchall()
|
||||
}
|
||||
|
||||
# Access-Reject entries
|
||||
cursor.execute("""
|
||||
SELECT p.username, d.description, p.reply, p.authdate
|
||||
FROM radpostauth p
|
||||
LEFT JOIN rad_description d ON p.username = d.username
|
||||
WHERE p.reply = 'Access-Reject'
|
||||
ORDER BY p.authdate DESC LIMIT 25
|
||||
""")
|
||||
reject_entries = cursor.fetchall()
|
||||
for row in reject_entries:
|
||||
normalized = row['username'].replace(":", "").replace("-", "").upper()
|
||||
row['vendor'] = lookup_vendor(row['username'])['vendor']
|
||||
row['ago'] = time_ago(row['authdate'])
|
||||
|
||||
if normalized in existing_user_map:
|
||||
row['already_exists'] = True
|
||||
row['existing_vlan'] = existing_user_map[normalized]
|
||||
else:
|
||||
row['already_exists'] = False
|
||||
row['existing_vlan'] = None
|
||||
print(f"⚠ Not found in radcheck: {row['username']} → {normalized}")
|
||||
|
||||
# Access-Accept entries
|
||||
cursor.execute("""
|
||||
SELECT p.username, d.description, p.reply, p.authdate
|
||||
FROM radpostauth p
|
||||
LEFT JOIN rad_description d ON p.username = d.username
|
||||
WHERE p.reply = 'Access-Accept'
|
||||
ORDER BY p.authdate DESC LIMIT 25
|
||||
""")
|
||||
accept_entries = cursor.fetchall()
|
||||
for row in accept_entries:
|
||||
row['vendor'] = lookup_vendor(row['username'])['vendor']
|
||||
row['ago'] = time_ago(row['authdate'])
|
||||
|
||||
cursor.close()
|
||||
db.close()
|
||||
|
||||
return render_template('stats.html',
|
||||
accept_entries=accept_entries,
|
||||
reject_entries=reject_entries,
|
||||
available_groups=available_groups)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@index.route('/lookup_mac', methods=['POST'])
|
||||
def lookup_mac():
|
||||
mac = request.form.get('mac', '').strip()
|
||||
if not mac:
|
||||
return jsonify({"error": "MAC address is required"}), 400
|
||||
|
||||
return jsonify(lookup_vendor(mac))
|
||||
276
app/views/user_views.py
Normal file
276
app/views/user_views.py
Normal file
@@ -0,0 +1,276 @@
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, jsonify, flash
|
||||
from database import get_db
|
||||
import mysql.connector, os, time, requests
|
||||
|
||||
user = Blueprint('user', __name__) # ✅ Blueprint name = "user"
|
||||
|
||||
@user.route('/user_list')
|
||||
def user_list():
|
||||
db = get_db()
|
||||
if db is None:
|
||||
return "Database connection failed", 500
|
||||
|
||||
cursor = db.cursor(dictionary=True)
|
||||
try:
|
||||
# Get user info
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
r.username AS mac_address,
|
||||
rd.description AS description,
|
||||
ug.groupname AS vlan_id,
|
||||
mvc.vendor_name AS vendor
|
||||
FROM radcheck r
|
||||
LEFT JOIN radusergroup ug ON r.username = ug.username
|
||||
LEFT JOIN rad_description rd ON r.username = rd.username
|
||||
LEFT JOIN mac_vendor_cache mvc ON UPPER(REPLACE(REPLACE(r.username, ':', ''), '-', '')) LIKE CONCAT(mvc.mac_prefix, '%')
|
||||
""")
|
||||
results = cursor.fetchall()
|
||||
|
||||
# Get available groups
|
||||
cursor.execute("SELECT groupname FROM radgroupcheck")
|
||||
groups = [{'groupname': row['groupname']} for row in cursor.fetchall()]
|
||||
|
||||
cursor.close()
|
||||
db.close()
|
||||
return render_template('user_list_inline_edit.html', results=results, groups=groups)
|
||||
|
||||
except mysql.connector.Error as e:
|
||||
print(f"Database error: {e}")
|
||||
cursor.close()
|
||||
db.close()
|
||||
return "Database error", 500
|
||||
|
||||
|
||||
@user.route('/update_user', methods=['POST'])
|
||||
def update_user():
|
||||
mac_address = request.form['mac_address']
|
||||
description = request.form['description']
|
||||
vlan_id = request.form['vlan_id']
|
||||
new_mac_address = request.form.get('new_mac_address')
|
||||
|
||||
db = get_db()
|
||||
if db:
|
||||
cursor = db.cursor()
|
||||
try:
|
||||
db.autocommit = False
|
||||
|
||||
if new_mac_address and new_mac_address != mac_address:
|
||||
cursor.execute("""
|
||||
UPDATE radcheck
|
||||
SET username = %s, value = %s
|
||||
WHERE username = %s
|
||||
""", (new_mac_address, new_mac_address, mac_address))
|
||||
|
||||
cursor.execute("""
|
||||
UPDATE rad_description
|
||||
SET username = %s, description = %s
|
||||
WHERE username = %s
|
||||
""", (new_mac_address, description, mac_address))
|
||||
|
||||
cursor.execute("""
|
||||
UPDATE radusergroup
|
||||
SET username = %s, groupname = %s
|
||||
WHERE username = %s
|
||||
""", (new_mac_address, vlan_id, mac_address))
|
||||
else:
|
||||
cursor.execute("""
|
||||
UPDATE rad_description
|
||||
SET description = %s
|
||||
WHERE username = %s
|
||||
""", (description, mac_address))
|
||||
|
||||
cursor.execute("""
|
||||
UPDATE radusergroup
|
||||
SET groupname = %s
|
||||
WHERE username = %s
|
||||
""", (vlan_id, mac_address))
|
||||
|
||||
db.commit()
|
||||
db.autocommit = True
|
||||
cursor.close()
|
||||
return "success"
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
db.autocommit = True
|
||||
cursor.close()
|
||||
return str(e)
|
||||
finally:
|
||||
db.close()
|
||||
return "Database Connection Failed"
|
||||
|
||||
|
||||
@user.route('/delete_user/<mac_address>')
|
||||
def delete_user(mac_address):
|
||||
db = get_db()
|
||||
if db:
|
||||
cursor = db.cursor()
|
||||
try:
|
||||
db.autocommit = False
|
||||
cursor.execute("DELETE FROM rad_description WHERE username = %s", (mac_address,))
|
||||
cursor.execute("DELETE FROM radcheck WHERE username = %s", (mac_address,))
|
||||
cursor.execute("DELETE FROM radusergroup WHERE username = %s", (mac_address,))
|
||||
db.commit()
|
||||
cursor.close()
|
||||
db.close()
|
||||
return redirect(url_for('user.user_list'))
|
||||
except mysql.connector.Error as err:
|
||||
print(f"Database Error: {err}")
|
||||
db.rollback()
|
||||
cursor.close()
|
||||
db.close()
|
||||
return redirect(url_for('user.user_list'))
|
||||
return "Database Connection Failed"
|
||||
|
||||
|
||||
@user.route('/add_user', methods=['POST'])
|
||||
def add_user():
|
||||
try:
|
||||
data = request.get_json()
|
||||
mac_address = data.get('mac_address')
|
||||
description = data.get('description')
|
||||
vlan_id = data.get('vlan_id')
|
||||
|
||||
if not mac_address:
|
||||
return jsonify({'success': False, 'message': 'MAC Address is required'}), 400
|
||||
|
||||
db = get_db()
|
||||
if db is None:
|
||||
return jsonify({'success': False, 'message': 'Database connection failed'}), 500
|
||||
|
||||
cursor = db.cursor()
|
||||
try:
|
||||
db.autocommit = False
|
||||
|
||||
cursor.execute("SELECT username FROM radcheck WHERE username = %s", (mac_address,))
|
||||
if cursor.fetchone():
|
||||
return jsonify({'success': False, 'message': 'User already exists'}), 400
|
||||
|
||||
cursor.execute("""
|
||||
INSERT INTO radcheck (username, attribute, op, value)
|
||||
VALUES (%s, 'Cleartext-Password', ':=', %s)
|
||||
""", (mac_address, mac_address))
|
||||
|
||||
cursor.execute("""
|
||||
INSERT INTO rad_description (username, description)
|
||||
VALUES (%s, %s)
|
||||
""", (mac_address, description))
|
||||
|
||||
cursor.execute("""
|
||||
INSERT INTO radusergroup (username, groupname)
|
||||
VALUES (%s, %s)
|
||||
""", (mac_address, vlan_id))
|
||||
|
||||
db.commit()
|
||||
db.autocommit = True
|
||||
cursor.close()
|
||||
db.close()
|
||||
return jsonify({'success': True, 'message': 'User added successfully'})
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
db.autocommit = True
|
||||
cursor.close()
|
||||
db.close()
|
||||
return jsonify({'success': False, 'message': str(e)}), 500
|
||||
except Exception:
|
||||
return jsonify({'success': False, 'message': 'Unknown error'}), 500
|
||||
|
||||
@user.route('/add_from_reject', methods=['POST'])
|
||||
def add_from_reject():
|
||||
username = request.form.get('username')
|
||||
groupname = request.form.get('groupname')
|
||||
|
||||
if not username or not groupname:
|
||||
flash("Missing MAC address or group", "error")
|
||||
return redirect(url_for('index.stats'))
|
||||
|
||||
db = get_db()
|
||||
cursor = db.cursor()
|
||||
try:
|
||||
db.autocommit = False
|
||||
|
||||
# Check if already exists
|
||||
cursor.execute("SELECT username FROM radcheck WHERE username = %s", (username,))
|
||||
if cursor.fetchone():
|
||||
flash(f"{username} already exists", "info")
|
||||
else:
|
||||
cursor.execute("""
|
||||
INSERT INTO radcheck (username, attribute, op, value)
|
||||
VALUES (%s, 'Cleartext-Password', ':=', %s)
|
||||
""", (username, username))
|
||||
|
||||
cursor.execute("""
|
||||
INSERT INTO rad_description (username, description)
|
||||
VALUES (%s, '')
|
||||
""", (username,))
|
||||
|
||||
cursor.execute("""
|
||||
INSERT INTO radusergroup (username, groupname)
|
||||
VALUES (%s, %s)
|
||||
""", (username, groupname))
|
||||
|
||||
db.commit()
|
||||
flash(f"{username} added to group {groupname}", "success")
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
flash(f"Error: {str(e)}", "error")
|
||||
finally:
|
||||
db.autocommit = True
|
||||
cursor.close()
|
||||
db.close()
|
||||
|
||||
return redirect(url_for('index.stats'))
|
||||
|
||||
@user.route('/refresh_vendors', methods=['POST'])
|
||||
def refresh_vendors():
|
||||
db = get_db()
|
||||
cursor = db.cursor(dictionary=True)
|
||||
|
||||
api_url = os.getenv('MACLOOKUP_API_URL', 'https://api.maclookup.app/v2/macs/{}').strip('"')
|
||||
api_key = os.getenv('MACLOOKUP_API_KEY', '').strip('"')
|
||||
limit = int(os.getenv('MACLOOKUP_RATE_LIMIT', 2))
|
||||
headers = {'Authorization': f'Bearer {api_key}'} if api_key else {}
|
||||
|
||||
cursor.execute("""
|
||||
SELECT r.username
|
||||
FROM radcheck r
|
||||
LEFT JOIN mac_vendor_cache m ON UPPER(REPLACE(REPLACE(r.username, ':', ''), '-', '')) LIKE CONCAT(m.mac_prefix, '%')
|
||||
WHERE m.vendor_name IS NULL OR m.vendor_name = 'Unknown Vendor'
|
||||
LIMIT 5
|
||||
""")
|
||||
entries = cursor.fetchall()
|
||||
|
||||
if not entries:
|
||||
cursor.close()
|
||||
db.close()
|
||||
return jsonify({"success": True, "updated": 0, "remaining": False})
|
||||
|
||||
updated = 0
|
||||
for entry in entries:
|
||||
mac = entry['username']
|
||||
prefix = mac.replace(':', '').replace('-', '').upper()[:6]
|
||||
|
||||
try:
|
||||
r = requests.get(api_url.format(mac), headers=headers, timeout=3)
|
||||
if r.status_code == 200:
|
||||
data = r.json()
|
||||
vendor = data.get("company", "not found")
|
||||
|
||||
cursor.execute("""
|
||||
INSERT INTO mac_vendor_cache (mac_prefix, vendor_name, last_updated)
|
||||
VALUES (%s, %s, NOW())
|
||||
ON DUPLICATE KEY UPDATE vendor_name = VALUES(vendor_name), last_updated = NOW()
|
||||
""", (prefix, vendor))
|
||||
db.commit()
|
||||
updated += 1
|
||||
except Exception as e:
|
||||
print(f"Error for {mac}: {e}")
|
||||
|
||||
time.sleep(1 / limit)
|
||||
|
||||
cursor.close()
|
||||
db.close()
|
||||
|
||||
return jsonify({"success": True, "updated": updated, "remaining": True})
|
||||
Reference in New Issue
Block a user