getting there

This commit is contained in:
2025-04-06 14:39:32 -04:00
parent af7e24a948
commit 2e511ca428
7 changed files with 387 additions and 176 deletions

View File

@@ -29,4 +29,4 @@ COPY . .
EXPOSE 8080 EXPOSE 8080
# Run the app via Gunicorn # Run the app via Gunicorn
CMD ["gunicorn", "--bind", "0.0.0.0:8080", "wsgi:app"] CMD ["gunicorn", "--bind", "0.0.0.0:8080", "wsgi:app", "--timeout", "120", "--workers", "2"]

View File

@@ -1,6 +1,6 @@
from flask import current_app from flask import current_app
import mysql.connector import mysql.connector
import datetime from datetime import datetime, timedelta, timezone
import requests import requests
import time import time
import os import os
@@ -139,149 +139,83 @@ def delete_user(mac_address):
conn.close() conn.close()
def get_latest_auth_logs(reply_type=None, limit=5, time_range=None): def get_latest_auth_logs(reply_type=None, limit=5, time_range=None, offset=0):
conn = get_connection() conn = get_connection()
cursor = conn.cursor(dictionary=True) cursor = conn.cursor(dictionary=True)
# Determine the time filter based on the time_range now = datetime.now(pytz.timezone(current_app.config.get('APP_TIMEZONE', 'UTC')))
if time_range: time_filter = None
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: if time_range and time_range != 'all':
cursor.execute(""" delta = {
SELECT * FROM auth_logs 'last_minute': timedelta(minutes=1),
WHERE reply = %s AND timestamp >= %s 'last_5_minutes': timedelta(minutes=5),
ORDER BY timestamp DESC 'last_10_minutes': timedelta(minutes=10),
LIMIT %s 'last_hour': timedelta(hours=1),
""", (reply_type, time_filter, limit)) 'last_6_hours': timedelta(hours=6),
else: 'last_12_hours': timedelta(hours=12),
cursor.execute(""" 'last_day': timedelta(days=1),
SELECT * FROM auth_logs 'last_30_days': timedelta(days=30)
WHERE reply = %s }.get(time_range)
ORDER BY timestamp DESC if delta:
LIMIT %s time_filter = now - delta
""", (reply_type, limit))
if time_filter:
cursor.execute("""
SELECT * FROM auth_logs
WHERE reply = %s AND timestamp >= %s
ORDER BY timestamp DESC
LIMIT %s OFFSET %s
""", (reply_type, time_filter, limit, offset))
else: else:
cursor.execute(""" cursor.execute("""
SELECT * FROM auth_logs SELECT * FROM auth_logs
WHERE reply = %s WHERE reply = %s
ORDER BY timestamp DESC ORDER BY timestamp DESC
LIMIT %s LIMIT %s OFFSET %s
""", (reply_type, limit)) """, (reply_type, limit, offset))
logs = cursor.fetchall() logs = cursor.fetchall()
cursor.close() cursor.close()
conn.close() conn.close()
return logs return logs
def count_auth_logs(reply_type=None, time_range=None):
def get_vendor_info(mac, insert_if_found=True):
conn = get_connection() conn = get_connection()
cursor = conn.cursor(dictionary=True) cursor = conn.cursor()
prefix = mac.lower().replace(":", "").replace("-", "")[:6]
print(f">>> Looking up MAC: {mac} → Prefix: {prefix}") now = datetime.now(pytz.timezone(current_app.config.get('APP_TIMEZONE', 'UTC')))
print("→ Searching in local database...") time_filter = None
cursor.execute("SELECT vendor_name, status FROM mac_vendors WHERE mac_prefix = %s", (prefix,))
row = cursor.fetchone()
if row: if time_range and time_range != 'all':
print(f"✓ Found locally: {row['vendor_name']} (Status: {row['status']})") delta = {
cursor.close() 'last_minute': timedelta(minutes=1),
conn.close() 'last_5_minutes': timedelta(minutes=5),
return { 'last_10_minutes': timedelta(minutes=10),
"mac": mac, 'last_hour': timedelta(hours=1),
"vendor": row['vendor_name'], 'last_6_hours': timedelta(hours=6),
"source": "local", 'last_12_hours': timedelta(hours=12),
"status": row['status'] 'last_day': timedelta(days=1),
} 'last_30_days': timedelta(days=30)
}.get(time_range)
if delta:
time_filter = now - delta
print("✗ Not found locally, querying API...") if time_filter:
cursor.execute("""
SELECT COUNT(*) FROM auth_logs
WHERE reply = %s AND timestamp >= %s
""", (reply_type, time_filter))
else:
cursor.execute("""
SELECT COUNT(*) FROM auth_logs
WHERE reply = %s
""", (reply_type,))
url_template = current_app.config.get("OUI_API_URL", "https://api.maclookup.app/v2/macs/{}") count = cursor.fetchone()[0]
api_key = current_app.config.get("OUI_API_KEY", "") cursor.close()
headers = {"Authorization": f"Bearer {api_key}"} if api_key else {} conn.close()
return count
try:
url = url_template.format(prefix)
print(f"→ Querying API: {url}")
response = requests.get(url, headers=headers)
if response.status_code == 200:
data = response.json()
vendor = data.get("company", "").strip()
if vendor:
print(f"✓ Found from API: {vendor}")
if insert_if_found:
cursor.execute("""
INSERT INTO mac_vendors (mac_prefix, vendor_name, status, last_checked, last_updated)
VALUES (%s, %s, 'found', NOW(), NOW())
ON DUPLICATE KEY UPDATE
vendor_name = VALUES(vendor_name),
status = 'found',
last_checked = NOW(),
last_updated = NOW()
""", (prefix, vendor))
conn.commit()
print("→ Inserted into database (found).")
return {
"mac": mac,
"vendor": vendor,
"source": "api",
"status": "found"
}
elif response.status_code == 404:
print("✗ API returned 404 - vendor not found.")
if insert_if_found:
cursor.execute("""
INSERT INTO mac_vendors (mac_prefix, vendor_name, status, last_checked, last_updated)
VALUES (%s, %s, 'not_found', NOW(), NOW())
ON DUPLICATE KEY UPDATE
vendor_name = VALUES(vendor_name),
status = 'not_found',
last_checked = NOW(),
last_updated = NOW()
""", (prefix, "not found"))
conn.commit()
print("→ Inserted into database (not_found).")
return {
"mac": mac,
"vendor": "",
"source": "api",
"status": "not_found"
}
else:
print(f"✗ API error: {response.status_code}")
return {"mac": mac, "vendor": "", "error": f"API error: {response.status_code}"}
except Exception as e:
print(f"✗ Exception while querying API: {e}")
return {"mac": mac, "vendor": "", "error": str(e)}
finally:
cursor.close()
conn.close()
def get_summary_counts(): def get_summary_counts():
conn = get_connection() conn = get_connection()
@@ -458,3 +392,133 @@ def get_user_by_mac(mac_address):
cursor.close() cursor.close()
conn.close() conn.close()
return user return user
def get_known_mac_vendors():
conn = get_connection()
cursor = conn.cursor(dictionary=True)
cursor.execute("SELECT mac_prefix, vendor_name, status FROM mac_vendors")
entries = cursor.fetchall()
cursor.close()
conn.close()
return {
row['mac_prefix'].lower(): {
'vendor': row['vendor_name'],
'status': row['status']
}
for row in entries
}
def get_vendor_info(mac, insert_if_found=True):
conn = get_connection()
cursor = conn.cursor(dictionary=True)
prefix = mac.lower().replace(":", "").replace("-", "")[:6]
print(f">>> Looking up MAC: {mac} → Prefix: {prefix}")
print("→ Searching in local database...")
cursor.execute("SELECT vendor_name, status FROM mac_vendors WHERE mac_prefix = %s", (prefix,))
row = cursor.fetchone()
if row:
print(f"✓ Found locally: {row['vendor_name']} (Status: {row['status']})")
cursor.close()
conn.close()
return {
"mac": mac,
"vendor": row['vendor_name'],
"source": "local",
"status": row['status']
}
print("✗ Not found locally, querying API...")
url_template = current_app.config.get("OUI_API_URL", "https://api.maclookup.app/v2/macs/{}")
api_key = current_app.config.get("OUI_API_KEY", "")
headers = {"Authorization": f"Bearer {api_key}"} if api_key else {}
try:
url = url_template.format(prefix)
print(f"→ Querying API: {url}")
response = requests.get(url, headers=headers)
if response.status_code == 200:
data = response.json()
vendor = data.get("company", "").strip()
if vendor:
print(f"✓ Found from API: {vendor}")
# Always insert found results, even if insert_if_found=False
cursor.execute("""
INSERT INTO mac_vendors (mac_prefix, vendor_name, status, last_checked, last_updated)
VALUES (%s, %s, 'found', NOW(), NOW())
ON DUPLICATE KEY UPDATE
vendor_name = VALUES(vendor_name),
status = 'found',
last_checked = NOW(),
last_updated = NOW()
""", (prefix, vendor))
print(f"→ Inserted vendor: {vendor} → rowcount: {cursor.rowcount}")
conn.commit()
return {
"mac": mac,
"vendor": vendor,
"source": "api",
"status": "found"
}
else:
print("⚠️ API returned empty company field. Treating as not_found.")
# 🛠 Always insert not_found, even if insert_if_found=False
cursor.execute("""
INSERT INTO mac_vendors (mac_prefix, vendor_name, status, last_checked, last_updated)
VALUES (%s, %s, 'not_found', NOW(), NOW())
ON DUPLICATE KEY UPDATE
vendor_name = VALUES(vendor_name),
status = 'not_found',
last_checked = NOW(),
last_updated = NOW()
""", (prefix, "not found"))
print(f"→ Inserted not_found for {prefix} → rowcount: {cursor.rowcount}")
conn.commit()
return {
"mac": mac,
"vendor": "",
"source": "api",
"status": "not_found"
}
elif response.status_code == 404:
print("✗ API returned 404 - vendor not found.")
# 🛠 Always insert not_found
cursor.execute("""
INSERT INTO mac_vendors (mac_prefix, vendor_name, status, last_checked, last_updated)
VALUES (%s, %s, 'not_found', NOW(), NOW())
ON DUPLICATE KEY UPDATE
vendor_name = VALUES(vendor_name),
status = 'not_found',
last_checked = NOW(),
last_updated = NOW()
""", (prefix, "not found"))
print(f"→ Inserted not_found (404) for {prefix} → rowcount: {cursor.rowcount}")
conn.commit()
return {
"mac": mac,
"vendor": "",
"source": "api",
"status": "not_found"
}
else:
print(f"✗ API error: {response.status_code}")
return {"mac": mac, "vendor": "", "error": f"API error: {response.status_code}"}
except Exception as e:
print(f"✗ Exception while querying API: {e}")
return {"mac": mac, "vendor": "", "error": str(e)}
finally:
cursor.close()
conn.close()

View File

@@ -294,3 +294,26 @@ form.inline-form {
flex: 0 0 auto; flex: 0 0 auto;
padding: 6px; padding: 6px;
} }
.pagination {
margin-top: 0.75rem;
text-align: center;
}
.pagination a,
.pagination span.current-page {
display: inline-block;
padding: 4px 10px;
margin: 0 3px;
border: 1px solid var(--accent);
border-radius: 4px;
color: var(--fg);
background-color: transparent;
text-decoration: none;
}
.pagination span.current-page {
font-weight: bold;
background-color: var(--accent);
color: black;
}

View File

@@ -3,10 +3,9 @@
{% block content %} {% block content %}
<div class="stats-page"> <div class="stats-page">
<h1 class="page-title">Authentication Stats</h1> <h1 class="page-title">Authentication Stats</h1>
<form method="POST" action="/stats/stats"> <form method="POST" action="{{ url_for('stats.stats_page') }}">
<label for="time_range">Select Time Range:</label> <label for="time_range">Select Time Range:</label>
<select name="time_range" id="time_range"> <select name="time_range" id="time_range">
<option value="last_minute" {% if time_range == 'last_minute' %}selected{% endif %}>Last 1 Minute</option> <option value="last_minute" {% if time_range == 'last_minute' %}selected{% endif %}>Last 1 Minute</option>
@@ -23,6 +22,7 @@
</form> </form>
<div class="stats-container"> <div class="stats-container">
<!-- Access-Accept Card --> <!-- Access-Accept Card -->
<div class="card success-card"> <div class="card success-card">
<h2>Recent Access-Accept</h2> <h2>Recent Access-Accept</h2>
@@ -40,12 +40,23 @@
<tr> <tr>
<td>{{ entry.mac_address }}</td> <td>{{ entry.mac_address }}</td>
<td>{{ entry.description or '' }}</td> <td>{{ entry.description or '' }}</td>
<td>{{ entry.vendor }}</td> <td class="vendor-cell" data-mac="{{ entry.mac_address }}">{{ entry.vendor or '...' }}</td>
<td>{{ entry.ago }}</td> <td>{{ entry.ago }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
{% if total_pages_accept > 1 %}
<div class="pagination">
{% for page in range(1, total_pages_accept + 1) %}
{% if page == page_accept %}
<span class="current-page">{{ page }}</span>
{% else %}
<a href="{{ url_for('stats.stats_page', page_accept=page, page_reject=page_reject, page_fallback=page_fallback, time_range=time_range) }}">{{ page }}</a>
{% endif %}
{% endfor %}
</div>
{% endif %}
</div> </div>
<!-- Access-Reject Card --> <!-- Access-Reject Card -->
@@ -65,12 +76,23 @@
<tr> <tr>
<td>{{ entry.mac_address }}</td> <td>{{ entry.mac_address }}</td>
<td>{{ entry.description or '' }}</td> <td>{{ entry.description or '' }}</td>
<td>{{ entry.vendor }}</td> <td class="vendor-cell" data-mac="{{ entry.mac_address }}">{{ entry.vendor or '...' }}</td>
<td>{{ entry.ago }}</td> <td>{{ entry.ago }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
{% if total_pages_reject > 1 %}
<div class="pagination">
{% for page in range(1, total_pages_reject + 1) %}
{% if page == page_reject %}
<span class="current-page">{{ page }}</span>
{% else %}
<a href="{{ url_for('stats.stats_page', page_accept=page_accept, page_reject=page, page_fallback=page_fallback, time_range=time_range) }}">{{ page }}</a>
{% endif %}
{% endfor %}
</div>
{% endif %}
</div> </div>
<!-- Access-Fallback Card --> <!-- Access-Fallback Card -->
@@ -87,11 +109,9 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% if fallback_entries %}
{% for entry in fallback_entries %} {% for entry in fallback_entries %}
<tr> <tr>
<td>{{ entry.mac_address }}</td> <td>{{ entry.mac_address }}</td>
<td> <td>
{% if not entry.already_exists %} {% if not entry.already_exists %}
<input type="text" name="description" value="{{ entry.description or '' }}" placeholder="Description (optional)" form="form-{{ loop.index }}"> <input type="text" name="description" value="{{ entry.description or '' }}" placeholder="Description (optional)" form="form-{{ loop.index }}">
@@ -99,35 +119,73 @@
{{ entry.description or '' }} {{ entry.description or '' }}
{% endif %} {% endif %}
</td> </td>
<td class="vendor-cell" data-mac="{{ entry.mac_address }}">{{ entry.vendor or '...' }}</td>
<td>{{ entry.vendor }}</td>
<td>{{ entry.ago }}</td> <td>{{ entry.ago }}</td>
<td> <td>
{% if not entry.already_exists %} {% if not entry.already_exists %}
<form method="POST" action="{{ url_for('stats.add') }}" class="inline-form" id="form-{{ loop.index }}"> <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 }}"> <input type="hidden" name="mac_address" value="{{ entry.mac_address }}">
<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 available_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>
<button type="submit" title="Add">💾</button> <button type="submit" title="Add">💾</button>
</form> </form>
{% else %} {% else %}
<span style="color: limegreen;">Already exists in VLAN {{ entry.existing_vlan or 'unknown' }}</span> <span style="color: limegreen;">Already exists in VLAN {{ entry.existing_vlan or 'unknown' }}</span>
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
{% else %}
<tr><td colspan="5">No data available.</td></tr>
{% endif %}
</tbody> </tbody>
</table> </table>
{% if total_pages_fallback > 1 %}
<div class="pagination">
{% for page in range(1, total_pages_fallback + 1) %}
{% if page == page_fallback %}
<span class="current-page">{{ page }}</span>
{% else %}
<a href="{{ url_for('stats.stats_page', page_accept=page_accept, page_reject=page_reject, page_fallback=page, time_range=time_range) }}">{{ page }}</a>
{% endif %}
{% endfor %}
</div>
{% endif %}
</div> </div>
</div> </div>
</div>
</div> {# closes .stats-page #} <script>
document.addEventListener('DOMContentLoaded', () => {
const queriedPrefixes = new Set();
document.querySelectorAll('.vendor-cell').forEach(cell => {
const mac = cell.getAttribute('data-mac');
if (cell.textContent.trim() === '...') {
const prefix = mac.replace(/[^a-fA-F0-9]/g, '').substring(0, 6).toLowerCase();
if (queriedPrefixes.has(prefix)) return;
queriedPrefixes.add(prefix);
fetch('{{ url_for("stats.lookup_mac_async") }}', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mac })
})
.then(res => res.json())
.then(data => {
if (data.vendor) {
document.querySelectorAll(`.vendor-cell[data-mac^="${prefix}"]`).forEach(c => {
if (c.textContent.trim() === '...') {
c.textContent = data.vendor;
}
});
}
})
.catch(err => {
console.warn('MAC lookup failed:', err);
});
}
});
});
</script>
{% endblock %} {% endblock %}

View File

@@ -1,8 +1,11 @@
from flask import Blueprint, render_template, request, current_app, redirect, url_for from flask import Blueprint, render_template, request, current_app, redirect, url_for, jsonify
from db_interface import get_latest_auth_logs, get_all_groups, get_vendor_info, get_user_by_mac, add_user from db_interface import get_latest_auth_logs, count_auth_logs, get_all_groups, get_vendor_info, get_user_by_mac, add_user, get_known_mac_vendors
from math import ceil
import pytz import pytz
import humanize import humanize
from datetime import datetime, timezone, timedelta from datetime import datetime, timezone, timedelta
from time import sleep
import threading
stats = Blueprint('stats', __name__) stats = Blueprint('stats', __name__)
@@ -21,32 +24,47 @@ def get_time_filter_delta(time_range):
@stats.route('/stats', methods=['GET', 'POST']) @stats.route('/stats', methods=['GET', 'POST'])
def stats_page(): def stats_page():
time_range = request.form.get('time_range') or request.args.get('time_range') or 'last_minute' 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
# Per-card pagination values
per_page = 25
page_accept = int(request.args.get('page_accept', 1))
page_reject = int(request.args.get('page_reject', 1))
page_fallback = int(request.args.get('page_fallback', 1))
# Timezone setup # Timezone setup
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)
def is_within_selected_range(ts): # Accept pagination
if time_range == "all": total_accept = count_auth_logs('Access-Accept', time_range)
return True total_pages_accept = ceil(total_accept / per_page)
delta = get_time_filter_delta(time_range) offset_accept = (page_accept - 1) * per_page
if not delta or not ts: accept_entries = get_latest_auth_logs('Access-Accept', per_page, time_range, offset_accept)
return True
now = datetime.now(timezone.utc) # Reject pagination
if ts.tzinfo is None: total_reject = count_auth_logs('Access-Reject', time_range)
ts = ts.replace(tzinfo=timezone.utc) total_pages_reject = ceil(total_reject / per_page)
return (now - ts) <= delta offset_reject = (page_reject - 1) * per_page
reject_entries = get_latest_auth_logs('Access-Reject', per_page, time_range, offset_reject)
# Fallback pagination
total_fallback = count_auth_logs('Accept-Fallback', time_range)
total_pages_fallback = ceil(total_fallback / per_page)
offset_fallback = (page_fallback - 1) * per_page
fallback_entries = get_latest_auth_logs('Accept-Fallback', per_page, time_range, offset_fallback)
def enrich(entry): def enrich(entry):
if entry.get('timestamp') and entry['timestamp'].tzinfo is None: ts = entry.get('timestamp')
entry['timestamp'] = entry['timestamp'].replace(tzinfo=timezone.utc) if ts:
if ts.tzinfo is None:
ts = ts.replace(tzinfo=timezone.utc)
local_time = ts.astimezone(local_tz)
entry['ago'] = humanize.naturaltime(datetime.now(local_tz) - local_time)
else:
entry['ago'] = 'unknown'
local_time = entry['timestamp'].astimezone(local_tz) vendor_info = get_vendor_info(entry['mac_address'], insert_if_found=False)
entry['ago'] = humanize.naturaltime(datetime.now(local_tz) - local_time) entry['vendor'] = vendor_info['vendor'] if vendor_info else None # placeholder
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']) user = get_user_by_mac(entry['mac_address'])
entry['already_exists'] = user is not None entry['already_exists'] = user is not None
@@ -55,20 +73,29 @@ def stats_page():
return entry return entry
# Get and enrich logs after filtering # Enrich entries
accept_entries = [enrich(e) for e in get_latest_auth_logs('Access-Accept', limit) if is_within_selected_range(e.get('timestamp'))] accept_entries = [enrich(e) for e in accept_entries]
reject_entries = [enrich(e) for e in get_latest_auth_logs('Access-Reject', limit) if is_within_selected_range(e.get('timestamp'))] reject_entries = [enrich(e) for e in reject_entries]
fallback_entries = [enrich(e) for e in get_latest_auth_logs('Accept-Fallback', limit) if is_within_selected_range(e.get('timestamp'))] fallback_entries = [enrich(e) for e in fallback_entries]
available_groups = get_all_groups() available_groups = get_all_groups()
return render_template( return render_template(
"stats.html", "stats.html",
time_range=time_range,
accept_entries=accept_entries, accept_entries=accept_entries,
reject_entries=reject_entries, reject_entries=reject_entries,
fallback_entries=fallback_entries, fallback_entries=fallback_entries,
available_groups=available_groups, available_groups=available_groups,
time_range=time_range
page_accept=page_accept,
total_pages_accept=total_pages_accept,
page_reject=page_reject,
total_pages_reject=total_pages_reject,
page_fallback=page_fallback,
total_pages_fallback=total_pages_fallback
) )
@stats.route('/add', methods=['POST']) @stats.route('/add', methods=['POST'])
@@ -80,3 +107,42 @@ def add():
add_user(mac, desc, group_id) add_user(mac, desc, group_id)
return redirect(url_for('stats.stats_page')) return redirect(url_for('stats.stats_page'))
@stats.route('/lookup_mac_async', methods=['POST'])
def lookup_mac_async():
data = request.get_json()
macs = data.get('macs', [])
results = {}
rate_limit = int(current_app.config.get("OUI_API_LIMIT_PER_SEC", 2))
delay = 1.0 / rate_limit if rate_limit > 0 else 0.5
# Lowercase cleaned prefixes
prefixes_to_lookup = {}
for mac in macs:
prefix = mac.lower().replace(":", "").replace("-", "")[:6]
prefixes_to_lookup[prefix] = mac # Use last MAC that used this prefix
known_vendors = get_known_mac_vendors() # local DB cache
vendor_cache = {} # cache during this request
for prefix, mac in prefixes_to_lookup.items():
if prefix in known_vendors:
results[mac] = known_vendors[prefix]['vendor']
continue
if prefix in vendor_cache:
print(f"→ Prefix {prefix} already queried in this request, skipping.")
results[mac] = vendor_cache[prefix]
continue
info = get_vendor_info(mac) # will insert into DB
vendor_name = info.get('vendor', '')
vendor_cache[prefix] = vendor_name
results[mac] = vendor_name
sleep(delay) # throttle
return jsonify(results)

View File

@@ -7,8 +7,8 @@ user = Blueprint('user', __name__, url_prefix='/user')
@user.route('/') @user.route('/')
def user_list(): def user_list():
users = get_all_users() users = get_all_users()
groups = get_all_groups() available_groups = get_all_groups()
return render_template('user_list.html', users=users, groups=groups) return render_template('user_list.html', users=users, available_groups=available_groups)
@user.route('/add', methods=['POST']) @user.route('/add', methods=['POST'])