working for the most part
This commit is contained in:
Binary file not shown.
@@ -2,6 +2,7 @@ from flask import Flask, redirect, url_for, render_template
|
|||||||
from views.index_views import index
|
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 config import app_config
|
from config import app_config
|
||||||
|
|
||||||
|
|
||||||
@@ -24,6 +25,7 @@ app.logger.setLevel(logging.INFO)
|
|||||||
app.register_blueprint(index)
|
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.route('/user_list')
|
@app.route('/user_list')
|
||||||
def legacy_user_list():
|
def legacy_user_list():
|
||||||
@@ -33,10 +35,6 @@ def legacy_user_list():
|
|||||||
def legacy_group_list():
|
def legacy_group_list():
|
||||||
return redirect(url_for('group.group_list'))
|
return redirect(url_for('group.group_list'))
|
||||||
|
|
||||||
@app.route('/stats')
|
|
||||||
def stats():
|
|
||||||
return render_template('stats.html')
|
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
def index_redirect():
|
def index_redirect():
|
||||||
return render_template('index.html')
|
return render_template('index.html')
|
||||||
|
|||||||
11
app/db_connection.py
Normal file
11
app/db_connection.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import mysql.connector
|
||||||
|
import os
|
||||||
|
|
||||||
|
def get_connection():
|
||||||
|
return mysql.connector.connect(
|
||||||
|
host=os.getenv('DB_HOST'),
|
||||||
|
port=int(os.getenv('DB_PORT', 3306)),
|
||||||
|
user=os.getenv('DB_USER'),
|
||||||
|
password=os.getenv('DB_PASSWORD'),
|
||||||
|
database=os.getenv('DB_NAME'),
|
||||||
|
)
|
||||||
@@ -4,14 +4,8 @@ import datetime
|
|||||||
import requests
|
import requests
|
||||||
import time
|
import time
|
||||||
import os
|
import os
|
||||||
|
import pytz
|
||||||
def get_connection():
|
from db_connection import get_connection
|
||||||
return mysql.connector.connect(
|
|
||||||
host=current_app.config['DB_HOST'],
|
|
||||||
user=current_app.config['DB_USER'],
|
|
||||||
password=current_app.config['DB_PASSWORD'],
|
|
||||||
database=current_app.config['DB_NAME']
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_all_users():
|
def get_all_users():
|
||||||
@@ -45,10 +39,10 @@ def get_all_groups():
|
|||||||
FROM groups g
|
FROM groups g
|
||||||
ORDER BY g.vlan_id
|
ORDER BY g.vlan_id
|
||||||
""")
|
""")
|
||||||
groups = cursor.fetchall()
|
available_groups = cursor.fetchall()
|
||||||
cursor.close()
|
cursor.close()
|
||||||
conn.close()
|
conn.close()
|
||||||
return groups
|
return available_groups
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -106,6 +100,7 @@ def duplicate_group(vlan_id):
|
|||||||
|
|
||||||
|
|
||||||
def add_user(mac_address, description, vlan_id):
|
def add_user(mac_address, description, vlan_id):
|
||||||
|
print(f"→ Adding to DB: mac={mac_address}, desc={description}, vlan={vlan_id}")
|
||||||
conn = get_connection()
|
conn = get_connection()
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
@@ -144,19 +139,61 @@ def delete_user(mac_address):
|
|||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
def get_latest_auth_logs(result, limit=10):
|
def get_latest_auth_logs(reply_type=None, limit=5, time_range=None):
|
||||||
conn = get_connection()
|
conn = get_connection()
|
||||||
cursor = conn.cursor(dictionary=True)
|
cursor = conn.cursor(dictionary=True)
|
||||||
cursor.execute(
|
|
||||||
"SELECT * FROM auth_logs WHERE result = %s ORDER BY timestamp DESC LIMIT %s",
|
# Determine the time filter based on the time_range
|
||||||
(result, limit)
|
if time_range:
|
||||||
)
|
now = datetime.now(pytz.timezone(current_app.config.get('APP_TIMEZONE', 'UTC')))
|
||||||
|
if time_range == 'last_minute':
|
||||||
|
time_filter = now - timedelta(minutes=1)
|
||||||
|
elif time_range == 'last_5_minutes':
|
||||||
|
time_filter = now - timedelta(minutes=5)
|
||||||
|
elif time_range == 'last_10_minutes':
|
||||||
|
time_filter = now - timedelta(minutes=10)
|
||||||
|
elif time_range == 'last_hour':
|
||||||
|
time_filter = now - timedelta(hours=1)
|
||||||
|
elif time_range == 'last_6_hours':
|
||||||
|
time_filter = now - timedelta(hours=6)
|
||||||
|
elif time_range == 'last_12_hours':
|
||||||
|
time_filter = now - timedelta(hours=12)
|
||||||
|
elif time_range == 'last_day':
|
||||||
|
time_filter = now - timedelta(days=1)
|
||||||
|
elif time_range == 'last_30_days':
|
||||||
|
time_filter = now - timedelta(days=30)
|
||||||
|
else: # 'all' case
|
||||||
|
time_filter = None
|
||||||
|
|
||||||
|
if time_filter:
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT * FROM auth_logs
|
||||||
|
WHERE reply = %s AND timestamp >= %s
|
||||||
|
ORDER BY timestamp DESC
|
||||||
|
LIMIT %s
|
||||||
|
""", (reply_type, time_filter, limit))
|
||||||
|
else:
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT * FROM auth_logs
|
||||||
|
WHERE reply = %s
|
||||||
|
ORDER BY timestamp DESC
|
||||||
|
LIMIT %s
|
||||||
|
""", (reply_type, limit))
|
||||||
|
else:
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT * FROM auth_logs
|
||||||
|
WHERE reply = %s
|
||||||
|
ORDER BY timestamp DESC
|
||||||
|
LIMIT %s
|
||||||
|
""", (reply_type, limit))
|
||||||
|
|
||||||
logs = cursor.fetchall()
|
logs = cursor.fetchall()
|
||||||
cursor.close()
|
cursor.close()
|
||||||
conn.close()
|
conn.close()
|
||||||
return logs
|
return logs
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def get_vendor_info(mac, insert_if_found=True):
|
def get_vendor_info(mac, insert_if_found=True):
|
||||||
conn = get_connection()
|
conn = get_connection()
|
||||||
cursor = conn.cursor(dictionary=True)
|
cursor = conn.cursor(dictionary=True)
|
||||||
@@ -410,3 +447,14 @@ def lookup_mac_verbose(mac):
|
|||||||
conn.close()
|
conn.close()
|
||||||
return "\n".join(output)
|
return "\n".join(output)
|
||||||
|
|
||||||
|
def get_user_by_mac(mac_address):
|
||||||
|
conn = get_connection()
|
||||||
|
cursor = conn.cursor(dictionary=True)
|
||||||
|
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT * FROM users WHERE mac_address = %s
|
||||||
|
""", (mac_address,))
|
||||||
|
user = cursor.fetchone()
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
return user
|
||||||
|
|||||||
@@ -115,7 +115,12 @@ table.styled-table {
|
|||||||
.styled-table th {
|
.styled-table th {
|
||||||
background-color: var(--header-bg);
|
background-color: var(--header-bg);
|
||||||
color: var(--fg);
|
color: var(--fg);
|
||||||
text-align: left;
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 🧩 Fix: Remove right border from last column */
|
||||||
|
.styled-table thead th:last-child {
|
||||||
|
border-right: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.styled-table input[type="text"],
|
.styled-table input[type="text"],
|
||||||
@@ -236,3 +241,56 @@ form.inline-form {
|
|||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.stats-page .stats-container {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-page .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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-page .success-card {
|
||||||
|
border-left: 6px solid limegreen;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-page .error-card {
|
||||||
|
border-left: 6px solid crimson;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-page .fallback-card {
|
||||||
|
border-left: 6px solid orange;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-page .styled-table.small-table td,
|
||||||
|
.stats-page .styled-table.small-table th {
|
||||||
|
padding: 6px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-page form.inline-form {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-page form.inline-form select {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 140px;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-page form.inline-form button {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<a href="{{ url_for('index_redirect') }}">Home</a>
|
<a href="{{ url_for('index_redirect') }}">Home</a>
|
||||||
<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</a>
|
<a href="{{ url_for('stats.stats_page') }}">Stats</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="right">
|
<div class="right">
|
||||||
<button id="theme-toggle">🌓 Theme</button>
|
<button id="theme-toggle">🌓 Theme</button>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for group in groups %}
|
{% for group in available_groups %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ group.vlan_id }}</td>
|
<td>{{ group.vlan_id }}</td>
|
||||||
<td>
|
<td>
|
||||||
|
|||||||
@@ -15,30 +15,27 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2>Recent Access-Accept</h2>
|
<h2>Recent Access Logs</h2>
|
||||||
<ul class="event-list green">
|
<ul class="event-list">
|
||||||
{% for entry in latest_accept %}
|
<li><strong>Access-Accept Logs</strong></li>
|
||||||
|
{% for log in latest_accept %}
|
||||||
<li>
|
<li>
|
||||||
<strong>{{ entry.mac_address }}</strong>
|
<strong>{{ log.mac_address }}</strong> - {{ log.reply }}
|
||||||
{% if entry.description %} ({{ entry.description }}){% endif %}
|
<br>
|
||||||
— {{ entry.ago }}
|
<small>{{ log.timestamp }} - {{ log.result }}</small>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<li><strong>Access-Reject Logs</strong></li>
|
||||||
|
{% for log in latest_reject %}
|
||||||
|
<li>
|
||||||
|
<strong>{{ log.mac_address }}</strong> - {{ log.reply }}
|
||||||
|
<br>
|
||||||
|
<small>{{ log.timestamp }} - {{ log.result }}</small>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h2>Recent Access-Reject</h2>
|
|
||||||
<ul class="event-list red">
|
|
||||||
{% for entry in latest_reject %}
|
|
||||||
<li>
|
|
||||||
<strong>{{ entry.mac_address }}</strong>
|
|
||||||
{% if entry.description %} ({{ entry.description }}){% endif %}
|
|
||||||
— {{ entry.ago }}
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h2>MAC Vendor Lookup</h2>
|
<h2>MAC Vendor Lookup</h2>
|
||||||
<form id="mac-lookup-form" method="POST" action="/lookup_mac">
|
<form id="mac-lookup-form" method="POST" action="/lookup_mac">
|
||||||
<input type="text" name="mac" id="mac-input" placeholder="Enter MAC address" required>
|
<input type="text" name="mac" id="mac-input" placeholder="Enter MAC address" required>
|
||||||
|
|||||||
@@ -2,99 +2,132 @@
|
|||||||
{% block title %}Authentication Stats{% endblock %}
|
{% block title %}Authentication Stats{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
<div class="stats-page">
|
||||||
|
|
||||||
<h1 class="page-title">Authentication Stats</h1>
|
<h1 class="page-title">Authentication Stats</h1>
|
||||||
|
|
||||||
<div class="stats-container">
|
<form method="POST" action="/stats/stats">
|
||||||
<div class="card success-card">
|
<label for="time_range">Select Time Range:</label>
|
||||||
<h2>Recent Access-Accept</h2>
|
<select name="time_range" id="time_range">
|
||||||
<table class="styled-table small-table">
|
<option value="last_minute" {% if time_range == 'last_minute' %}selected{% endif %}>Last 1 Minute</option>
|
||||||
<thead>
|
<option value="last_5_minutes" {% if time_range == 'last_5_minutes' %}selected{% endif %}>Last 5 Minutes</option>
|
||||||
<tr>
|
<option value="last_10_minutes" {% if time_range == 'last_10_minutes' %}selected{% endif %}>Last 10 Minutes</option>
|
||||||
<th>MAC Address</th>
|
<option value="last_hour" {% if time_range == 'last_hour' %}selected{% endif %}>Last Hour</option>
|
||||||
<th>Description</th>
|
<option value="last_6_hours" {% if time_range == 'last_6_hours' %}selected{% endif %}>Last 6 Hours</option>
|
||||||
<th>Vendor</th>
|
<option value="last_12_hours" {% if time_range == 'last_12_hours' %}selected{% endif %}>Last 12 Hours</option>
|
||||||
<th>Time</th>
|
<option value="last_day" {% if time_range == 'last_day' %}selected{% endif %}>Last Day</option>
|
||||||
</tr>
|
<option value="last_30_days" {% if time_range == 'last_30_days' %}selected{% endif %}>Last 30 Days</option>
|
||||||
</thead>
|
<option value="all" {% if time_range == 'all' %}selected{% endif %}>All Time</option>
|
||||||
<tbody>
|
</select>
|
||||||
{% for entry in accept_entries %}
|
<button type="submit">Update</button>
|
||||||
<tr>
|
</form>
|
||||||
<td>{{ entry.mac_address }}</td>
|
|
||||||
<td>{{ entry.description or '' }}</td>
|
|
||||||
<td>{{ entry.vendor }}</td>
|
|
||||||
<td>{{ entry.ago }}</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card error-card">
|
<div class="stats-container">
|
||||||
<h2>Recent Access-Reject</h2>
|
<!-- Access-Accept Card -->
|
||||||
<table class="styled-table small-table">
|
<div class="card success-card">
|
||||||
<thead>
|
<h2>Recent Access-Accept</h2>
|
||||||
<tr>
|
<table class="styled-table small-table">
|
||||||
<th>MAC Address</th>
|
<thead>
|
||||||
<th>Description</th>
|
<tr>
|
||||||
<th>Vendor</th>
|
<th>MAC Address</th>
|
||||||
<th>Time</th>
|
<th>Description</th>
|
||||||
<th>Actions</th>
|
<th>Vendor</th>
|
||||||
</tr>
|
<th>Time</th>
|
||||||
</thead>
|
</tr>
|
||||||
<tbody>
|
</thead>
|
||||||
{% for entry in reject_entries %}
|
<tbody>
|
||||||
<tr>
|
{% for entry in accept_entries %}
|
||||||
<td>{{ entry.mac_address }}</td>
|
<tr>
|
||||||
<td>{{ entry.description or '' }}</td>
|
<td>{{ entry.mac_address }}</td>
|
||||||
<td>{{ entry.vendor }}</td>
|
<td>{{ entry.description or '' }}</td>
|
||||||
<td>{{ entry.ago }}</td>
|
<td>{{ entry.vendor }}</td>
|
||||||
<td>
|
<td>{{ entry.ago }}</td>
|
||||||
{% if entry.already_exists %}
|
</tr>
|
||||||
<span style="color: limegreen;">Already exists in VLAN {{ entry.existing_vlan or 'unknown' }}</span>
|
{% endfor %}
|
||||||
{% else %}
|
</tbody>
|
||||||
<form method="POST" action="/user/add_from_reject" style="display: flex; gap: 4px;">
|
</table>
|
||||||
<input type="hidden" name="mac_address" value="{{ entry.mac_address }}">
|
</div>
|
||||||
<select name="group_id" required>
|
|
||||||
<option value="">Select VLAN</option>
|
<!-- Access-Reject Card -->
|
||||||
{% for group in available_groups %}
|
<div class="card error-card">
|
||||||
<option value="{{ group.id }}">VLAN {{ group.vlan_id }}</option>
|
<h2>Recent Access-Reject</h2>
|
||||||
{% endfor %}
|
<table class="styled-table small-table">
|
||||||
</select>
|
<thead>
|
||||||
<button type="submit" title="Add User">💾</button>
|
<tr>
|
||||||
</form>
|
<th>MAC Address</th>
|
||||||
{% endif %}
|
<th>Description</th>
|
||||||
</td>
|
<th>Vendor</th>
|
||||||
</tr>
|
<th>Time</th>
|
||||||
{% endfor %}
|
</tr>
|
||||||
</tbody>
|
</thead>
|
||||||
</table>
|
<tbody>
|
||||||
</div>
|
{% for entry in reject_entries %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ entry.mac_address }}</td>
|
||||||
|
<td>{{ entry.description or '' }}</td>
|
||||||
|
<td>{{ entry.vendor }}</td>
|
||||||
|
<td>{{ entry.ago }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Access-Fallback Card -->
|
||||||
|
<div class="card fallback-card">
|
||||||
|
<h2>Recent Access-Fallback</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>
|
||||||
|
{% if fallback_entries %}
|
||||||
|
{% for entry in fallback_entries %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ entry.mac_address }}</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
{% if not entry.already_exists %}
|
||||||
|
<input type="text" name="description" value="{{ entry.description or '' }}" placeholder="Description (optional)" form="form-{{ loop.index }}">
|
||||||
|
{% else %}
|
||||||
|
{{ entry.description or '' }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>{{ entry.vendor }}</td>
|
||||||
|
<td>{{ entry.ago }}</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
{% if not entry.already_exists %}
|
||||||
|
<form method="POST" action="{{ url_for('stats.add') }}" class="inline-form" id="form-{{ loop.index }}">
|
||||||
|
<input type="hidden" name="mac_address" value="{{ entry.mac_address }}">
|
||||||
|
<select name="group_id" required>
|
||||||
|
<option value="">Assign to VLAN</option>
|
||||||
|
{% for group in available_groups %}
|
||||||
|
<option value="{{ group.vlan_id }}">VLAN {{ group.vlan_id }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<button type="submit" title="Add">💾</button>
|
||||||
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<span style="color: limegreen;">Already exists in VLAN {{ entry.existing_vlan or 'unknown' }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<tr><td colspan="5">No data available.</td></tr>
|
||||||
|
{% endif %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
</div> {# closes .stats-page #}
|
||||||
.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 %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<input type="text" name="description" placeholder="Description (optional)">
|
<input type="text" name="description" placeholder="Description (optional)">
|
||||||
<select name="group_id" required>
|
<select name="group_id" required>
|
||||||
<option value="">Assign to VLAN</option>
|
<option value="">Assign to VLAN</option>
|
||||||
{% for group in groups %}
|
{% for group in available_groups %}
|
||||||
<option value="{{ group.vlan_id }}">VLAN {{ group.vlan_id }}</option>
|
<option value="{{ group.vlan_id }}">VLAN {{ group.vlan_id }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
<form method="POST" action="{{ url_for('user.update_vlan_route') }}" class="inline-form">
|
<form method="POST" action="{{ url_for('user.update_vlan_route') }}" class="inline-form">
|
||||||
<input type="hidden" name="mac_address" value="{{ entry.mac_address }}">
|
<input type="hidden" name="mac_address" value="{{ entry.mac_address }}">
|
||||||
<select name="group_id" onchange="this.form.submit()">
|
<select name="group_id" onchange="this.form.submit()">
|
||||||
{% for group in groups %}
|
{% for group in available_groups %}
|
||||||
<option value="{{ group.vlan_id }}" {% if group.vlan_id == entry.vlan_id %}selected{% endif %}>
|
<option value="{{ group.vlan_id }}" {% if group.vlan_id == entry.vlan_id %}selected{% endif %}>
|
||||||
VLAN {{ group.vlan_id }}
|
VLAN {{ group.vlan_id }}
|
||||||
</option>
|
</option>
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -6,8 +6,8 @@ group = Blueprint('group', __name__, url_prefix='/group')
|
|||||||
|
|
||||||
@group.route('/')
|
@group.route('/')
|
||||||
def group_list():
|
def group_list():
|
||||||
groups = get_all_groups()
|
available_groups = get_all_groups()
|
||||||
return render_template('group_list.html', groups=groups)
|
return render_template('group_list.html', available_groups=available_groups)
|
||||||
|
|
||||||
|
|
||||||
@group.route('/add', methods=['POST'])
|
@group.route('/add', methods=['POST'])
|
||||||
|
|||||||
@@ -3,9 +3,8 @@ from datetime import datetime
|
|||||||
from db_interface import (
|
from db_interface import (
|
||||||
get_connection,
|
get_connection,
|
||||||
get_vendor_info,
|
get_vendor_info,
|
||||||
get_all_groups,
|
|
||||||
get_latest_auth_logs,
|
get_latest_auth_logs,
|
||||||
lookup_mac_verbose
|
get_all_groups,
|
||||||
)
|
)
|
||||||
import pytz
|
import pytz
|
||||||
|
|
||||||
@@ -15,7 +14,6 @@ def time_ago(dt):
|
|||||||
if not dt:
|
if not dt:
|
||||||
return "n/a"
|
return "n/a"
|
||||||
|
|
||||||
# Use configured timezone
|
|
||||||
tz_name = current_app.config.get('APP_TIMEZONE', 'UTC')
|
tz_name = current_app.config.get('APP_TIMEZONE', 'UTC')
|
||||||
local_tz = pytz.timezone(tz_name)
|
local_tz = pytz.timezone(tz_name)
|
||||||
|
|
||||||
@@ -51,31 +49,6 @@ def homepage():
|
|||||||
latest_accept=latest_accept,
|
latest_accept=latest_accept,
|
||||||
latest_reject=latest_reject)
|
latest_reject=latest_reject)
|
||||||
|
|
||||||
@index.route('/stats')
|
|
||||||
def stats():
|
|
||||||
accept_entries = get_latest_auth_logs('Access-Accept')
|
|
||||||
reject_entries = get_latest_auth_logs('Access-Reject')
|
|
||||||
available_groups = get_all_groups()
|
|
||||||
|
|
||||||
# Process entries to add vendor and time-ago
|
|
||||||
from datetime import datetime, timezone
|
|
||||||
import humanize
|
|
||||||
|
|
||||||
def enrich(entry):
|
|
||||||
from db_interface import get_vendor_info, get_user_by_mac
|
|
||||||
entry['vendor'] = get_vendor_info(entry['mac_address'])
|
|
||||||
entry['ago'] = humanize.naturaltime(datetime.now(timezone.utc) - entry['timestamp'])
|
|
||||||
user = get_user_by_mac(entry['mac_address'])
|
|
||||||
entry['already_exists'] = user is not None
|
|
||||||
entry['existing_vlan'] = user['vlan_id'] if user else None
|
|
||||||
entry['description'] = user['description'] if user else None
|
|
||||||
return entry
|
|
||||||
|
|
||||||
accept_entries = [enrich(e) for e in accept_entries]
|
|
||||||
reject_entries = [enrich(e) for e in reject_entries]
|
|
||||||
|
|
||||||
return render_template("stats.html", accept_entries=accept_entries, reject_entries=reject_entries, available_groups=available_groups)
|
|
||||||
|
|
||||||
@index.route('/lookup_mac', methods=['POST'])
|
@index.route('/lookup_mac', methods=['POST'])
|
||||||
def lookup_mac():
|
def lookup_mac():
|
||||||
mac = request.form.get('mac', '').strip()
|
mac = request.form.get('mac', '').strip()
|
||||||
@@ -85,7 +58,6 @@ def lookup_mac():
|
|||||||
result = lookup_mac_verbose(mac)
|
result = lookup_mac_verbose(mac)
|
||||||
return jsonify({"mac": mac, "output": result})
|
return jsonify({"mac": mac, "output": result})
|
||||||
|
|
||||||
|
|
||||||
def get_summary_counts():
|
def get_summary_counts():
|
||||||
conn = get_connection()
|
conn = get_connection()
|
||||||
cursor = conn.cursor(dictionary=True)
|
cursor = conn.cursor(dictionary=True)
|
||||||
|
|||||||
82
app/views/stats_views.py
Normal file
82
app/views/stats_views.py
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
from flask import Blueprint, render_template, request, current_app, redirect, url_for
|
||||||
|
from db_interface import get_latest_auth_logs, get_all_groups, get_vendor_info, get_user_by_mac, add_user
|
||||||
|
import pytz
|
||||||
|
import humanize
|
||||||
|
from datetime import datetime, timezone, timedelta
|
||||||
|
|
||||||
|
stats = Blueprint('stats', __name__)
|
||||||
|
|
||||||
|
def get_time_filter_delta(time_range):
|
||||||
|
return {
|
||||||
|
"last_minute": timedelta(minutes=1),
|
||||||
|
"last_5_minutes": timedelta(minutes=5),
|
||||||
|
"last_10_minutes": timedelta(minutes=10),
|
||||||
|
"last_hour": timedelta(hours=1),
|
||||||
|
"last_6_hours": timedelta(hours=6),
|
||||||
|
"last_12_hours": timedelta(hours=12),
|
||||||
|
"last_day": timedelta(days=1),
|
||||||
|
"last_30_days": timedelta(days=30),
|
||||||
|
}.get(time_range)
|
||||||
|
|
||||||
|
@stats.route('/stats', methods=['GET', 'POST'])
|
||||||
|
def stats_page():
|
||||||
|
time_range = request.form.get('time_range') or request.args.get('time_range') or 'last_minute'
|
||||||
|
limit = 1000 # Fetch enough to allow filtering by time later
|
||||||
|
|
||||||
|
# Timezone setup
|
||||||
|
tz_name = current_app.config.get('APP_TIMEZONE', 'UTC')
|
||||||
|
local_tz = pytz.timezone(tz_name)
|
||||||
|
|
||||||
|
def is_within_selected_range(ts):
|
||||||
|
if time_range == "all":
|
||||||
|
return True
|
||||||
|
delta = get_time_filter_delta(time_range)
|
||||||
|
if not delta or not ts:
|
||||||
|
return True
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
if ts.tzinfo is None:
|
||||||
|
ts = ts.replace(tzinfo=timezone.utc)
|
||||||
|
return (now - ts) <= delta
|
||||||
|
|
||||||
|
def enrich(entry):
|
||||||
|
if entry.get('timestamp') and entry['timestamp'].tzinfo is None:
|
||||||
|
entry['timestamp'] = entry['timestamp'].replace(tzinfo=timezone.utc)
|
||||||
|
|
||||||
|
local_time = entry['timestamp'].astimezone(local_tz)
|
||||||
|
entry['ago'] = humanize.naturaltime(datetime.now(local_tz) - local_time)
|
||||||
|
|
||||||
|
vendor_info = get_vendor_info(entry['mac_address']) or {}
|
||||||
|
entry['vendor'] = vendor_info.get('vendor', 'Unknown Vendor')
|
||||||
|
|
||||||
|
user = get_user_by_mac(entry['mac_address'])
|
||||||
|
entry['already_exists'] = user is not None
|
||||||
|
entry['existing_vlan'] = user['vlan_id'] if user else None
|
||||||
|
entry['description'] = user['description'] if user else None
|
||||||
|
|
||||||
|
return entry
|
||||||
|
|
||||||
|
# Get and enrich logs after filtering
|
||||||
|
accept_entries = [enrich(e) for e in get_latest_auth_logs('Access-Accept', limit) if is_within_selected_range(e.get('timestamp'))]
|
||||||
|
reject_entries = [enrich(e) for e in get_latest_auth_logs('Access-Reject', limit) if is_within_selected_range(e.get('timestamp'))]
|
||||||
|
fallback_entries = [enrich(e) for e in get_latest_auth_logs('Accept-Fallback', limit) if is_within_selected_range(e.get('timestamp'))]
|
||||||
|
|
||||||
|
available_groups = get_all_groups()
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
"stats.html",
|
||||||
|
accept_entries=accept_entries,
|
||||||
|
reject_entries=reject_entries,
|
||||||
|
fallback_entries=fallback_entries,
|
||||||
|
available_groups=available_groups,
|
||||||
|
time_range=time_range
|
||||||
|
)
|
||||||
|
|
||||||
|
@stats.route('/add', methods=['POST'])
|
||||||
|
def add():
|
||||||
|
mac = request.form['mac_address']
|
||||||
|
desc = request.form.get('description', '')
|
||||||
|
group_id = request.form.get('group_id') # keep as string since VARCHAR
|
||||||
|
current_app.logger.info(f"Received MAC={mac}, DESC={desc}, VLAN={group_id}")
|
||||||
|
|
||||||
|
add_user(mac, desc, group_id)
|
||||||
|
return redirect(url_for('stats.stats_page'))
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
from flask import Blueprint, render_template, request, redirect, url_for
|
from flask import Blueprint, render_template, request, redirect, url_for, flash
|
||||||
from db_interface import get_all_users, get_all_groups, add_user, update_description, update_vlan, delete_user, refresh_vendors
|
from db_interface import get_all_users, get_all_groups, add_user, update_description, update_vlan, delete_user, refresh_vendors, get_user_by_mac
|
||||||
|
|
||||||
user = Blueprint('user', __name__, url_prefix='/user')
|
user = Blueprint('user', __name__, url_prefix='/user')
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
CREATE TABLE IF NOT EXISTS users (
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
mac_address CHAR(12) NOT NULL PRIMARY KEY CHECK (mac_address REGEXP '^[0-9A-Fa-f]{12}$'),
|
mac_address CHAR(12) NOT NULL PRIMARY KEY CHECK (mac_address REGEXP '^[0-9A-Fa-f]{12}$'),
|
||||||
description VARCHAR(200),
|
description VARCHAR(200),
|
||||||
vlan_id VARCHAR(64)
|
vlan_id VARCHAR(64) NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Table for auth logs
|
-- Table for auth logs
|
||||||
|
|||||||
Reference in New Issue
Block a user