Compare commits
3 Commits
00a91eb556
...
196a1f31d3
| Author | SHA1 | Date | |
|---|---|---|---|
| 196a1f31d3 | |||
| bb121ccbc6 | |||
| 32ad2fd115 |
@@ -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
|
||||||
|
|||||||
50
README.md
50
README.md
@@ -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.
|
||||||
@@ -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 && \
|
||||||
|
|||||||
@@ -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'))
|
||||||
@@ -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
|
||||||
@@ -632,4 +635,94 @@ def get_summary_counts():
|
|||||||
cursor.close()
|
cursor.close()
|
||||||
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()
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
67
app/templates/maintenance.html
Normal file
67
app/templates/maintenance.html
Normal 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 %}
|
||||||
50
app/views/maintenance_views.py
Normal file
50
app/views/maintenance_views.py
Normal 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
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user