Compare commits

..

3 Commits

Author SHA1 Message Date
196a1f31d3 update readme 2025-04-07 13:22:09 -04:00
bb121ccbc6 updated! 2025-04-07 13:01:56 -04:00
32ad2fd115 added some database maintenance functions and a page 2025-04-07 12:54:14 -04:00
10 changed files with 291 additions and 15 deletions

View File

@@ -3,9 +3,10 @@ FLASK_SECRET_KEY=your-secret-key
# MariaDB container # MariaDB container
MYSQL_HOST=db MYSQL_HOST=db
MYSQL_ROOT_PASSWORD=radpass
MYSQL_DATABASE=radius MYSQL_DATABASE=radius
MYSQL_USER=radiususer MYSQL_USER=radius
MYSQL_PASSWORD=radiuspass MYSQL_PASSWORD=radpass
# MAC Lookup API # MAC Lookup API
OUI_API_KEY= # only required if you want to increase the OUI limits OUI_API_KEY= # only required if you want to increase the OUI limits
@@ -22,13 +23,6 @@ LOG_FILE_PATH=/app/logs/app.log
# Timezone # Timezone
APP_TIMEZONE=America/Toronto APP_TIMEZONE=America/Toronto
# Database config
DB_HOST=db
DB_PORT=3306
DB_USER=radiususer
DB_PASSWORD=radiuspass
DB_NAME=radius
# RADIUS config # RADIUS config
RADIUS_SECRET=changeme RADIUS_SECRET=changeme
RADIUS_PORT=1812 RADIUS_PORT=1812

View File

@@ -1 +1,49 @@
Need rewrite 🛡️ RadMac — Web Manager and radius server for MAC-based authentication / VLAN Assignment
RadMac is a lightweight Flask web UI for managing MAC address-based access control and VLAN assignment, backed by a MariaDB/MySQL database. It incorporate a lightweight radius server.
✨ Some Features
🔐 MAC-based User Management
Add/edit/delete MAC entries with descriptions and VLAN IDs.
🧠 MAC Vendor Lookup
Auto-lookup vendors using maclookup.app with rate-limited API integration and local caching.
📊 Auth Log Viewer
Filter Access-Accept / Reject / Fallback events with timestamps, MAC, vendor, and description.
🧹 Database Maintenance Tools
- View row counts for all tables
- Clear auth logs
- Backup the full database as a .sql file
- Restore from uploaded .sql files
🌗 Dark & Light Theme
Toggle between light and dark modes, with theme persistence.
🔁 Session-Friendly UX
Preserves scroll position, sticky headers, toast notifications.
📦 Setup (Docker Compose)
The project includes a ready-to-use docker-compose.yml.
1. Clone the repository
bash
Copy
Edit
git clone https://github.com/Simon-CR/RadMac.git
cd RadMac
2. Create environment file
Copy .env.template to .env and edit:
- Fill in your MySQL credentials and other optional settings like OUI_API_KEY.
3. Run the stack
docker-compose up --build
The web UI will be available at: http://localhost:8080
📄 License
MIT — do whatever you want, no guarantees.

View File

@@ -9,7 +9,7 @@ ENV TZ=$TIMEZONE
# Install tzdata and optional tools # Install tzdata and optional tools
RUN apt-get update && \ RUN apt-get update && \
apt-get install -y --no-install-recommends tzdata iputils-ping telnet && \ apt-get install -y --no-install-recommends tzdata iputils-ping telnet mariadb-client && \
ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && \ ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && \
echo $TZ > /etc/timezone && \ echo $TZ > /etc/timezone && \
apt-get clean && \ apt-get clean && \

View File

@@ -3,6 +3,7 @@ from views.index_views import index
from views.user_views import user from views.user_views import user
from views.group_views import group from views.group_views import group
from views.stats_views import stats from views.stats_views import stats
from views.maintenance_views import maintenance
from config import app_config from config import app_config
@@ -26,6 +27,7 @@ app.register_blueprint(index)
app.register_blueprint(user, url_prefix='/user') app.register_blueprint(user, url_prefix='/user')
app.register_blueprint(group, url_prefix='/group') app.register_blueprint(group, url_prefix='/group')
app.register_blueprint(stats, url_prefix='/stats') app.register_blueprint(stats, url_prefix='/stats')
app.register_blueprint(maintenance, url_prefix='/maintenance')
@app.route('/user_list') @app.route('/user_list')
def legacy_user_list(): def legacy_user_list():
@@ -38,3 +40,7 @@ def legacy_group_list():
@app.route('/') @app.route('/')
def index_redirect(): def index_redirect():
return render_template('index.html') return render_template('index.html')
@app.route('/maintenance')
def maintenance():
return redirect(url_for('maintenance.maintenance'))

View File

@@ -1,11 +1,14 @@
from flask import current_app, request, redirect, url_for, flash from flask import current_app, request, redirect, url_for, flash
import mysql.connector from db_connection import get_connection
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
import mysql.connector
import requests import requests
import time import time
import os import os
import subprocess
import pytz import pytz
from db_connection import get_connection # Assuming db_connection.py exists and defines get_connection import shutil
# ------------------------------ # ------------------------------
# User Management Functions # User Management Functions
@@ -633,3 +636,93 @@ def get_summary_counts():
conn.close() conn.close()
return total_users, total_groups return total_users, total_groups
# ------------------------------
# Maintenance Functions
# ------------------------------
def clear_auth_logs():
"""Route to clear authentication logs."""
from db_connection import get_connection
conn = get_connection()
cursor = conn.cursor()
try:
cursor.execute("DELETE FROM auth_logs")
conn.commit()
flash("✅ Authentication logs cleared.", "success")
except Exception as e:
conn.rollback()
flash(f"❌ Error clearing logs: {e}", "error")
finally:
cursor.close()
conn.close()
return redirect(url_for("maintenance.maintenance_page"))
def backup_database():
"""Create a SQL backup of the entire database and return the path to the file."""
conn = get_connection()
db_name = conn.database
user = conn.user
password = conn._password
host = conn.server_host if hasattr(conn, 'server_host') else 'localhost'
conn.close()
# Check if mysqldump exists
if not shutil.which("mysqldump"):
raise Exception("'mysqldump' command not found. Please install mariadb-client or mysql-client.")
backup_file = "backup.sql"
try:
with open(backup_file, "w") as f:
subprocess.run(
["mysqldump", "-h", host, "-u", user, f"-p{password}", db_name],
stdout=f,
check=True
)
except subprocess.CalledProcessError as e:
raise Exception(f"❌ Backup failed: {e}")
return backup_file
def restore_database(sql_content):
"""Restore the database from raw SQL content (as string)."""
conn = get_connection()
cursor = conn.cursor()
try:
for statement in sql_content.split(';'):
stmt = statement.strip()
if stmt:
cursor.execute(stmt)
conn.commit()
flash("✅ Database restored successfully.", "success")
except Exception as e:
conn.rollback()
flash(f"❌ Error restoring database: {e}", "error")
finally:
cursor.close()
conn.close()
return redirect(url_for("maintenance.maintenance_page"))
def get_table_stats():
"""Return a dictionary of table names and their row counts."""
conn = get_connection()
cursor = conn.cursor()
try:
cursor.execute("SHOW TABLES")
tables = [row[0] for row in cursor.fetchall()]
stats = {}
for table in tables:
cursor.execute(f"SELECT COUNT(*) FROM `{table}`")
count = cursor.fetchone()[0]
stats[table] = count
return stats
except Exception as e:
print(f"❌ Error retrieving table stats: {e}")
return None
finally:
cursor.close()
conn.close()

View File

@@ -402,3 +402,20 @@ form.inline-form {
font-size: 0.9rem; font-size: 0.9rem;
background: var(--cell-bg); background: var(--cell-bg);
} }
.flash-messages {
margin: 1em 0;
}
.alert {
padding: 1em;
border-radius: 8px;
margin-bottom: 1em;
}
.alert-success {
background-color: #d4edda;
color: #155724;
}
.alert-error {
background-color: #f8d7da;
color: #721c24;
}

View File

@@ -13,6 +13,7 @@
<a href="{{ url_for('user.user_list') }}">Users</a> <a href="{{ url_for('user.user_list') }}">Users</a>
<a href="{{ url_for('group.group_list') }}">Groups</a> <a href="{{ url_for('group.group_list') }}">Groups</a>
<a href="{{ url_for('stats.stats_page') }}">Stats</a> <a href="{{ url_for('stats.stats_page') }}">Stats</a>
<a href="{{ url_for('maintenance.maintenance_page') }}">Maintenance</a>
</div> </div>
<div class="right"> <div class="right">
<button id="theme-toggle">🌓 Theme</button> <button id="theme-toggle">🌓 Theme</button>

View File

@@ -0,0 +1,67 @@
{% extends 'base.html' %}
{% block title %}Maintenance{% endblock %}
{% block content %}
<h1>Database Maintenance</h1>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="flash-messages">
{% for category, message in messages %}
<div class="alert alert-{{ category }}">{{ message }}</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
<p>Perform common database maintenance tasks here.</p>
<hr>
<h2>Database Statistics</h2>
<div class="database-stats">
{% if table_stats %}
{% for table, row_count in table_stats.items() %}
<div class="card">
<div class="card-header">{{ table }}</div>
<div class="card-body">
<p>Number of rows: {{ row_count }}</p>
</div>
</div>
{% endfor %}
{% else %}
<p>Could not retrieve database statistics.</p>
{% endif %}
</div>
<hr>
<h2>Clear Authentication Logs</h2>
<p>Permanently remove all authentication logs from the database. This action cannot be undone.</p>
<form action="/maintenance/clear_auth_logs" method="post">
<button type="submit" class="btn btn-danger" onclick="return confirm('Are you sure you want to clear all authentication logs? This action is irreversible!')">
Clear Logs
</button>
</form>
<hr>
<h2>Database Backup</h2>
<p>Create a backup of the current database. The backup will be saved as a SQL file.</p>
<p style="color: red;">
Warning: Database backups can be very large if you do not clear the authentication logs first.
</p>
<form action="/maintenance/backup_database" method="get">
<button type="submit" class="btn btn-primary">Backup Database</button>
</form>
<hr>
<h2>Database Restore</h2>
<p>Restore the database from a previously created SQL backup file. This will overwrite the current database.</p>
<form action="/maintenance/restore_database" method="post" enctype="multipart/form-data">
<input type="file" name="file" accept=".sql" required>
<button type="submit" class="btn btn-danger" onclick="return confirm('Are you sure you want to restore the database from this file? This will OVERWRITE the current database.')">
Restore Database
</button>
</form>
{% endblock %}

View File

@@ -0,0 +1,50 @@
from flask import Blueprint, render_template, request, send_file
import mysql.connector
import os
from db_interface import clear_auth_logs, backup_database, restore_database, get_table_stats # Import the functions from db_interface.py
maintenance = Blueprint('maintenance', __name__, url_prefix='/maintenance')
@maintenance.route('/')
def maintenance_page():
"""Renders the maintenance page."""
table_stats = get_table_stats()
return render_template('maintenance.html', table_stats=table_stats)
@maintenance.route('/clear_auth_logs', methods=['POST'])
def clear_auth_logs_route():
"""Route to clear authentication logs."""
return clear_auth_logs()
@maintenance.route('/backup_database', methods=['GET'])
def backup_database_route():
"""Route to backup the database."""
try:
backup_file = backup_database()
return send_file(backup_file, as_attachment=True, download_name='database_backup.sql')
except Exception as e:
return str(e), 500
finally:
if os.path.exists('backup.sql'):
os.remove('backup.sql')
@maintenance.route('/restore_database', methods=['POST'])
def restore_database_route():
"""Route to restore the database."""
if 'file' not in request.files:
return "No file provided", 400
sql_file = request.files['file']
if sql_file.filename == '':
return "No file selected", 400
if not sql_file.filename.endswith('.sql'):
return "Invalid file type. Only .sql files are allowed.", 400
try:
sql_content = sql_file.read().decode('utf-8')
message = restore_database(sql_content)
return message
except Exception as e:
return str(e), 500

View File

@@ -43,7 +43,7 @@ services:
image: adminer image: adminer
restart: unless-stopped restart: unless-stopped
ports: ports:
- "8081:8080" # Access at http://localhost:8081 - "8081:8080"
app: app:
build: build:
context: ./app context: ./app