getting there
This commit is contained in:
@@ -1,245 +1,152 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<link rel="icon" href="/static/favicon.ico" type="image/x-icon">
|
||||
<meta name="description" content="FreeRADIUS Web Manager">
|
||||
<meta name="author" content="Simon Cloutier">
|
||||
<meta property="og:title" content="FreeRADIUS Manager">
|
||||
<meta property="og:description" content="Manage FreeRADIUS MAC authentication visually">
|
||||
<meta property="og:type" content="website">
|
||||
<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;
|
||||
}
|
||||
<meta charset="UTF-8">
|
||||
<title>{% block title %}RadMac{% endblock %}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>
|
||||
:root {
|
||||
--bg: #121212;
|
||||
--fg: #f0f0f0;
|
||||
--accent: #4caf50;
|
||||
--cell-bg: #1e1e1e;
|
||||
--card-bg: #2c2c2c;
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
--bg: #121212;
|
||||
--fg: #e0e0e0;
|
||||
--cell-bg: #1e1e1e;
|
||||
--th-bg: #2c2c2c;
|
||||
}
|
||||
[data-theme="light"] {
|
||||
--bg: #f8f9fa;
|
||||
--fg: #212529;
|
||||
--accent: #28a745;
|
||||
--cell-bg: #ffffff;
|
||||
--card-bg: #e9ecef;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--bg);
|
||||
color: var(--fg);
|
||||
font-family: sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
body {
|
||||
background-color: var(--bg);
|
||||
color: var(--fg);
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
margin: 0;
|
||||
padding: 1rem 2rem;
|
||||
}
|
||||
|
||||
nav {
|
||||
background-color: var(--th-bg);
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
nav {
|
||||
background-color: var(--card-bg);
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid #666;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
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 .links a {
|
||||
margin-right: 1rem;
|
||||
color: var(--fg);
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
nav a.active {
|
||||
background-color: var(--cell-bg);
|
||||
}
|
||||
nav .links a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 20px;
|
||||
}
|
||||
nav .right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#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);
|
||||
}
|
||||
button#theme-toggle {
|
||||
background: none;
|
||||
border: 1px solid var(--fg);
|
||||
padding: 4px 8px;
|
||||
color: var(--fg);
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.styled-table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin-bottom: 2rem;
|
||||
background-color: var(--bg);
|
||||
color: var(--fg);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
h1, h2, h3 {
|
||||
color: var(--fg);
|
||||
}
|
||||
|
||||
.styled-table th,
|
||||
.styled-table td {
|
||||
border: 1px solid #444;
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
}
|
||||
.toast {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
background-color: var(--accent);
|
||||
color: white;
|
||||
padding: 10px 16px;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.4);
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s ease;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
.toast.show {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.styled-table input:focus,
|
||||
.styled-table select:focus {
|
||||
background-color: #555;
|
||||
outline: none;
|
||||
}
|
||||
table.styled-table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin-top: 1rem;
|
||||
background-color: var(--cell-bg);
|
||||
}
|
||||
|
||||
.styled-table thead {
|
||||
background-color: var(--th-bg);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
.styled-table th, .styled-table td {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #555;
|
||||
}
|
||||
|
||||
.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>
|
||||
.styled-table th {
|
||||
background-color: var(--card-bg);
|
||||
color: var(--fg);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<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>
|
||||
<nav>
|
||||
<div class="links">
|
||||
<a href="/">Home</a>
|
||||
<a href="/user_list">Users</a>
|
||||
<a href="/group">Groups</a>
|
||||
<a href="/stats">Stats</a>
|
||||
</div>
|
||||
<div class="right">
|
||||
<button id="theme-toggle">🌓 Theme</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="content">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
|
||||
<div id="toast-container"></div>
|
||||
<div id="toast" class="toast"></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);
|
||||
}
|
||||
<script>
|
||||
// Theme toggle logic
|
||||
const toggleBtn = document.getElementById('theme-toggle');
|
||||
const userPref = localStorage.getItem('theme');
|
||||
|
||||
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>
|
||||
|
||||
if (userPref) {
|
||||
document.documentElement.setAttribute('data-theme', userPref);
|
||||
}
|
||||
|
||||
toggleBtn.addEventListener('click', () => {
|
||||
const current = document.documentElement.getAttribute('data-theme');
|
||||
const next = current === 'light' ? 'dark' : 'light';
|
||||
document.documentElement.setAttribute('data-theme', next);
|
||||
localStorage.setItem('theme', next);
|
||||
});
|
||||
|
||||
// Toast display function
|
||||
function showToast(message, duration = 3000) {
|
||||
const toast = document.getElementById('toast');
|
||||
toast.textContent = message;
|
||||
toast.classList.add('show');
|
||||
setTimeout(() => toast.classList.remove('show'), duration);
|
||||
}
|
||||
|
||||
// Make toast function globally available
|
||||
window.showToast = showToast;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,183 +1,52 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}Group List{% endblock %}
|
||||
{% block title %}VLAN Groups{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="page-title">Group List</h1>
|
||||
<h1 class="page-title">VLAN Groups</h1>
|
||||
|
||||
<table class="styled-table fade-in">
|
||||
<form method="POST" action="{{ url_for('group.add_group_route') }}" style="margin-bottom: 1rem;">
|
||||
<input type="text" name="vlan_id" placeholder="VLAN ID" required pattern="[0-9]+" style="width: 80px;">
|
||||
<input type="text" name="description" placeholder="Group Description">
|
||||
<button type="submit">➕ Add Group</button>
|
||||
</form>
|
||||
|
||||
<table class="styled-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Group Name</th>
|
||||
<th>Attribute</th>
|
||||
<th>Op</th>
|
||||
<th>Value</th>
|
||||
<th>VLAN ID</th>
|
||||
<th>Description</th>
|
||||
<th>User Count</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() %}
|
||||
<tbody>
|
||||
{% for group in groups %}
|
||||
<tr>
|
||||
<td><input type="text" id="groupname-{{ groupname }}" value="{{ groupname }}" disabled></td>
|
||||
<td colspan="3" class="merged-cell"></td>
|
||||
<td>{{ group.vlan_id }}</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>
|
||||
<form method="POST" action="{{ url_for('group.update_description_route') }}" class="inline-form">
|
||||
<input type="hidden" name="group_id" value="{{ group.vlan_id }}">
|
||||
<input type="text" name="description" value="{{ group.description or '' }}">
|
||||
<button type="submit" title="Save">💾</button>
|
||||
</form>
|
||||
</td>
|
||||
<td>{{ group.user_count }}</td>
|
||||
<td>
|
||||
<form method="POST" action="{{ url_for('group.delete_group_route') }}" onsubmit="return confirm('Delete this group?');">
|
||||
<input type="hidden" name="group_id" value="{{ group.vlan_id }}">
|
||||
<button type="submit">❌</button>
|
||||
</form>
|
||||
</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>
|
||||
|
||||
<script>
|
||||
function enableEdit(groupname) {
|
||||
const input = document.getElementById(`groupname-${groupname}`);
|
||||
input.disabled = false;
|
||||
input.focus();
|
||||
<style>
|
||||
form.inline-form {
|
||||
display: inline-flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
function saveScrollPosition() {
|
||||
sessionStorage.setItem("scrollPosition", window.scrollY);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
function saveNewGroup() {
|
||||
const groupname = document.getElementById("new-groupname").value;
|
||||
const attributes = [];
|
||||
const attrInputs = document.querySelectorAll(".new-attribute");
|
||||
|
||||
attrInputs.forEach((attrInput, index) => {
|
||||
const attribute = attrInput.value;
|
||||
const op = document.querySelectorAll(".new-op")[index].value;
|
||||
const value = document.querySelectorAll(".new-value")[index].value;
|
||||
|
||||
if (attribute && op && value) {
|
||||
attributes.push({ attribute, op, value });
|
||||
}
|
||||
});
|
||||
|
||||
if (!groupname || attributes.length === 0) {
|
||||
showToast("Group name and at least one attribute required.");
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
const oldAttrRows = document.querySelectorAll(".new-attribute-row");
|
||||
oldAttrRows.forEach(row => row.remove());
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
document.getElementById("new-groupname").scrollIntoView({ behavior: 'smooth' });
|
||||
showToast("Fields populated from duplicated group.");
|
||||
});
|
||||
}
|
||||
|
||||
window.onload = function () {
|
||||
const scrollPosition = sessionStorage.getItem("scrollPosition");
|
||||
if (scrollPosition) {
|
||||
window.scrollTo(0, parseInt(scrollPosition) - 100);
|
||||
sessionStorage.removeItem("scrollPosition");
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}FreeRADIUS Manager{% endblock %}
|
||||
{% block title %}RadMac{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="page-title">FreeRADIUS Manager</h1>
|
||||
<h1 class="page-title">RadMac</h1>
|
||||
|
||||
<div class="stats-cards">
|
||||
<div class="card neutral">
|
||||
<strong>Total Users</strong>
|
||||
<strong>Total MAC Addresses</strong>
|
||||
<p>{{ total_users }}</p>
|
||||
</div>
|
||||
<div class="card neutral">
|
||||
<strong>Total Groups</strong>
|
||||
<strong>Total VLAN Groups</strong>
|
||||
<p>{{ total_groups }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Recent Access Accepts</h2>
|
||||
<h2>Recent Access-Accept</h2>
|
||||
<ul class="event-list green">
|
||||
{% for entry in latest_accept %}
|
||||
<li>
|
||||
<strong>{{ entry.username }}</strong>
|
||||
<strong>{{ entry.mac_address }}</strong>
|
||||
{% if entry.description %} ({{ entry.description }}){% endif %}
|
||||
— {{ entry.ago }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<h2>Recent Access Rejects</h2>
|
||||
<h2>Recent Access-Reject</h2>
|
||||
<ul class="event-list red">
|
||||
{% for entry in latest_reject %}
|
||||
<li>
|
||||
<strong>{{ entry.username }}</strong>
|
||||
<strong>{{ entry.mac_address }}</strong>
|
||||
{% if entry.description %} ({{ entry.description }}){% endif %}
|
||||
— {{ entry.ago }}
|
||||
</li>
|
||||
@@ -74,7 +74,6 @@ document.getElementById('mac-lookup-form').addEventListener('submit', function(e
|
||||
margin-bottom: 1rem;
|
||||
color: var(--fg);
|
||||
}
|
||||
|
||||
.stats-cards {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
@@ -101,7 +100,6 @@ document.getElementById('mac-lookup-form').addEventListener('submit', function(e
|
||||
}
|
||||
.event-list.green li { color: #4caf50; }
|
||||
.event-list.red li { color: #ff4d4d; }
|
||||
|
||||
#mac-lookup-form input {
|
||||
padding: 6px;
|
||||
border-radius: 4px;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
<div class="stats-container">
|
||||
<div class="card success-card">
|
||||
<h2>Last Access-Accept Events</h2>
|
||||
<h2>Recent Access-Accept</h2>
|
||||
<table class="styled-table small-table">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -19,7 +19,7 @@
|
||||
<tbody>
|
||||
{% for entry in accept_entries %}
|
||||
<tr>
|
||||
<td>{{ entry.username }}</td>
|
||||
<td>{{ entry.mac_address }}</td>
|
||||
<td>{{ entry.description or '' }}</td>
|
||||
<td>{{ entry.vendor }}</td>
|
||||
<td>{{ entry.ago }}</td>
|
||||
@@ -30,7 +30,7 @@
|
||||
</div>
|
||||
|
||||
<div class="card error-card">
|
||||
<h2>Last Access-Reject Events</h2>
|
||||
<h2>Recent Access-Reject</h2>
|
||||
<table class="styled-table small-table">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -44,20 +44,20 @@
|
||||
<tbody>
|
||||
{% for entry in reject_entries %}
|
||||
<tr>
|
||||
<td>{{ entry.username }}</td>
|
||||
<td>{{ entry.mac_address }}</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>
|
||||
<span style="color: limegreen;">Already exists in VLAN {{ entry.existing_vlan or 'unknown' }}</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>
|
||||
<input type="hidden" name="mac_address" value="{{ entry.mac_address }}">
|
||||
<select name="group_id" required>
|
||||
<option value="">Select VLAN</option>
|
||||
{% for group in available_groups %}
|
||||
<option value="{{ group }}">{{ group }}</option>
|
||||
<option value="{{ group.id }}">VLAN {{ group.vlan_id }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button type="submit" title="Add User">💾</button>
|
||||
|
||||
@@ -1,61 +1,62 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}User List{% endblock %}
|
||||
{% block title %}MAC Address List{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="page-title">User List</h1>
|
||||
<h1 class="page-title">MAC Address List</h1>
|
||||
|
||||
<table class="styled-table fade-in">
|
||||
<form id="add-user-form" method="POST" action="{{ url_for('user.add') }}">
|
||||
<input type="text" name="mac_address" placeholder="MAC address (12 hex characters)" required maxlength="12">
|
||||
<input type="text" name="description" placeholder="Description (optional)">
|
||||
<select name="group_id" required>
|
||||
<option value="">Assign to VLAN</option>
|
||||
{% for group in groups %}
|
||||
<option value="{{ group.id }}">VLAN {{ group.vlan_id }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button type="submit">➕ Add</button>
|
||||
</form>
|
||||
|
||||
<table class="styled-table">
|
||||
<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>Vendor <button id="refresh-vendors" title="Refresh unknown vendors">🔄</button></th>
|
||||
<th>VLAN</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>
|
||||
|
||||
{% for row in results %}
|
||||
<tbody>
|
||||
{% for entry in users %}
|
||||
<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>{{ entry.mac_address }}</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>
|
||||
<form method="POST" action="{{ url_for('user.update_description_route') }}" class="inline-form">
|
||||
<input type="hidden" name="mac_address" value="{{ entry.mac_address }}">
|
||||
<input type="text" name="description" value="{{ entry.description or '' }}">
|
||||
<button type="submit" title="Save">💾</button>
|
||||
</form>
|
||||
</td>
|
||||
<td>{% if entry.vendor_name %}
|
||||
{{ entry.vendor_name }}
|
||||
{% else %}
|
||||
<em>Unknown</em>
|
||||
{% endif %}</td>
|
||||
<td>
|
||||
<form method="POST" action="{{ url_for('user.update_vlan_route') }}" class="inline-form">
|
||||
<input type="hidden" name="mac_address" value="{{ entry.mac_address }}">
|
||||
<select name="group_id" onchange="this.form.submit()">
|
||||
{% for group in groups %}
|
||||
<option value="{{ group.id }}" {% if group.vlan_id == entry.vlan_id %}selected{% endif %}>VLAN {{ group.vlan_id }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</form>
|
||||
</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>
|
||||
<form method="POST" action="{{ url_for('user.delete') }}">
|
||||
<input type="hidden" name="mac_address" value="{{ entry.mac_address }}">
|
||||
<button type="submit" onclick="return confirm('Delete this MAC address?')">❌</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
@@ -63,117 +64,22 @@
|
||||
</table>
|
||||
|
||||
<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.");
|
||||
});
|
||||
}
|
||||
|
||||
refreshCycle();
|
||||
}
|
||||
|
||||
function saveScrollPosition() {
|
||||
sessionStorage.setItem("scrollPosition", window.scrollY);
|
||||
}
|
||||
|
||||
window.onload = function () {
|
||||
const scroll = sessionStorage.getItem("scrollPosition");
|
||||
if (scroll) {
|
||||
window.scrollTo(0, parseInt(scroll) - 100);
|
||||
sessionStorage.removeItem("scrollPosition");
|
||||
}
|
||||
};
|
||||
document.getElementById('refresh-vendors').addEventListener('click', function () {
|
||||
fetch("{{ url_for('user.refresh') }}", { method: "POST" })
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
alert("Vendor refresh complete.");
|
||||
window.location.reload();
|
||||
})
|
||||
.catch(err => alert("Error: " + err));
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
form.inline-form {
|
||||
display: inline-flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user