{% extends "base.twig" %}
{% block title %}{{ t('ui.admin.insecure_nodes.page_title', {}, locale, ['common']) }}{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2"><i class="fas fa-shield-alt me-2"></i>{{ t('ui.admin.insecure_nodes.heading', {}, locale, ['common']) }}</h1>
<div class="btn-toolbar">
<button class="btn btn-primary me-2" data-bs-toggle="modal" data-bs-target="#addNodeModal">
<i class="fas fa-plus me-1"></i>{{ t('ui.admin.insecure_nodes.add_node', {}, locale, ['common']) }}
</button>
<a href="/admin" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-1"></i>{{ t('ui.admin.insecure_nodes.back_to_dashboard', {}, locale, ['common']) }}
</a>
</div>
</div>
<!-- Info Alert -->
<div class="alert alert-info mb-4">
<i class="fas fa-info-circle me-2"></i>
<strong>{{ t('ui.admin.insecure_nodes.about_insecure_sessions', {}, locale, ['common']) }}</strong> {{ t('ui.admin.insecure_nodes.info_text_1', {}, locale, ['common']) }}
{{ t('ui.admin.insecure_nodes.info_text_2', {}, locale, ['common']) }}
</div>
<!-- Nodes Table -->
<div class="card shadow mb-4">
<div class="card-header py-3 d-flex justify-content-between align-items-center">
<h6 class="m-0 font-weight-bold text-primary">{{ t('ui.admin.insecure_nodes.allowed_nodes', {}, locale, ['common']) }}</h6>
<button class="btn btn-sm btn-outline-primary" id="refreshNodes">
<i class="fas fa-sync-alt"></i>
</button>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover" id="nodesTable">
<thead>
<tr>
<th>{{ t('ui.admin.insecure_nodes.address', {}, locale, ['common']) }}</th>
<th>{{ t('ui.common.description', {}, locale, ['common']) }}</th>
<th>{{ t('ui.admin.insecure_nodes.receive', {}, locale, ['common']) }}</th>
<th>{{ t('ui.admin.insecure_nodes.send', {}, locale, ['common']) }}</th>
<th>{{ t('ui.admin.insecure_nodes.max_msgs', {}, locale, ['common']) }}</th>
<th>{{ t('ui.admin.insecure_nodes.last_session', {}, locale, ['common']) }}</th>
<th>{{ t('ui.admin.insecure_nodes.active', {}, locale, ['common']) }}</th>
<th>{{ t('ui.admin.insecure_nodes.actions', {}, locale, ['common']) }}</th>
</tr>
</thead>
<tbody id="nodesTableBody">
<tr>
<td colspan="8" class="text-center">{{ t('ui.common.loading', {}, locale, ['common']) }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Add Node Modal -->
<div class="modal fade" id="addNodeModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{ t('ui.admin.insecure_nodes.add_node_to_allowlist', {}, locale, ['common']) }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="addNodeForm">
<div class="mb-3">
<label class="form-label">{{ t('ui.admin.insecure_nodes.ftn_address', {}, locale, ['common']) }} <span class="text-danger">*</span></label>
<input type="text" class="form-control" name="address" placeholder="{{ t('ui.admin.insecure_nodes.address_placeholder', {}, locale, ['common']) }}" required>
<div class="form-text">{{ t('ui.admin.insecure_nodes.ftn_address_help', {}, locale, ['common']) }}</div>
</div>
<div class="mb-3">
<label class="form-label">{{ t('ui.common.description', {}, locale, ['common']) }}</label>
<input type="text" class="form-control" name="description" placeholder="{{ t('ui.admin.insecure_nodes.description_placeholder', {}, locale, ['common']) }}">
</div>
<div class="mb-3">
<label class="form-label">{{ t('ui.admin.insecure_nodes.max_messages_per_session', {}, locale, ['common']) }}</label>
<input type="number" class="form-control" name="max_messages_per_session" value="100" min="1" max="10000">
</div>
<div class="mb-3">
<div class="form-check">
<input type="checkbox" class="form-check-input" name="allow_receive" id="allowReceive" checked>
<label class="form-check-label" for="allowReceive">{{ t('ui.admin.insecure_nodes.allow_receive', {}, locale, ['common']) }}</label>
<div class="form-text">{{ t('ui.admin.insecure_nodes.allow_receive_help', {}, locale, ['common']) }}</div>
</div>
</div>
<div class="mb-3">
<div class="form-check">
<input type="checkbox" class="form-check-input" name="allow_send" id="allowSend">
<label class="form-check-label" for="allowSend">{{ t('ui.admin.insecure_nodes.allow_send_pickup', {}, locale, ['common']) }}</label>
<div class="form-text">{{ t('ui.admin.insecure_nodes.allow_send_help', {}, locale, ['common']) }}</div>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ t('ui.common.cancel', {}, locale, ['common']) }}</button>
<button type="button" class="btn btn-primary" id="saveNodeBtn">{{ t('ui.admin.insecure_nodes.add_node', {}, locale, ['common']) }}</button>
</div>
</div>
</div>
</div>
<!-- Edit Node Modal -->
<div class="modal fade" id="editNodeModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{ t('ui.admin.insecure_nodes.edit_node', {}, locale, ['common']) }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="editNodeForm">
<input type="hidden" name="id" id="editNodeId">
<div class="mb-3">
<label class="form-label">{{ t('ui.admin.insecure_nodes.ftn_address', {}, locale, ['common']) }}</label>
<input type="text" class="form-control" name="address" id="editAddress" readonly>
</div>
<div class="mb-3">
<label class="form-label">{{ t('ui.common.description', {}, locale, ['common']) }}</label>
<input type="text" class="form-control" name="description" id="editDescription">
</div>
<div class="mb-3">
<label class="form-label">{{ t('ui.admin.insecure_nodes.max_messages_per_session', {}, locale, ['common']) }}</label>
<input type="number" class="form-control" name="max_messages_per_session" id="editMaxMessages" min="1" max="10000">
</div>
<div class="mb-3">
<div class="form-check">
<input type="checkbox" class="form-check-input" name="allow_receive" id="editAllowReceive">
<label class="form-check-label" for="editAllowReceive">{{ t('ui.admin.insecure_nodes.allow_receive', {}, locale, ['common']) }}</label>
</div>
</div>
<div class="mb-3">
<div class="form-check">
<input type="checkbox" class="form-check-input" name="allow_send" id="editAllowSend">
<label class="form-check-label" for="editAllowSend">{{ t('ui.admin.insecure_nodes.allow_send_pickup', {}, locale, ['common']) }}</label>
</div>
</div>
<div class="mb-3">
<div class="form-check">
<input type="checkbox" class="form-check-input" name="is_active" id="editIsActive">
<label class="form-check-label" for="editIsActive">{{ t('ui.admin.insecure_nodes.active', {}, locale, ['common']) }}</label>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ t('ui.common.cancel', {}, locale, ['common']) }}</button>
<button type="button" class="btn btn-primary" id="updateNodeBtn">{{ t('ui.admin.insecure_nodes.save_changes', {}, locale, ['common']) }}</button>
</div>
</div>
</div>
</div>
<script>
function apiMessage(payload, fallback) {
if (window.getApiMessage) {
return window.getApiMessage(payload, fallback);
}
return payload && payload.message ? payload.message : fallback;
}
function apiError(payload, fallback) {
if (window.getApiErrorMessage) {
return window.getApiErrorMessage(payload, fallback);
}
return payload && payload.error ? payload.error : fallback;
}
function uiT(key, fallback, params = {}) {
if (window.t) {
return window.t(key, params, fallback);
}
return fallback;
}
document.addEventListener('DOMContentLoaded', function() {
loadI18nNamespaces(['common', 'errors']).then(function() {
loadNodes();
});
document.getElementById('refreshNodes').addEventListener('click', loadNodes);
document.getElementById('saveNodeBtn').addEventListener('click', addNode);
document.getElementById('updateNodeBtn').addEventListener('click', updateNode);
});
function loadNodes() {
fetch('/admin/api/insecure-nodes')
.then(response => response.json())
.then(data => {
const tbody = document.getElementById('nodesTableBody');
if (!data.nodes || data.nodes.length === 0) {
tbody.innerHTML = '<tr><td colspan="8" class="text-center text-muted">' + uiT('ui.admin.insecure_nodes.no_nodes', 'No nodes in allowlist') + '</td></tr>';
return;
}
tbody.innerHTML = data.nodes.map(node => `
<tr>
<td><code>${escapeHtml(node.address)}</code></td>
<td>${escapeHtml(node.description || '-')}</td>
<td>${node.allow_receive ? '<i class="fas fa-check text-success"></i>' : '<i class="fas fa-times text-danger"></i>'}</td>
<td>${node.allow_send ? '<i class="fas fa-check text-success"></i>' : '<i class="fas fa-times text-danger"></i>'}</td>
<td>${node.max_messages_per_session}</td>
<td>${node.last_session_at ? formatDate(node.last_session_at) : '<em>' + uiT('ui.common.never', 'Never') + '</em>'}</td>
<td>
${node.is_active
? '<span class="badge bg-success">' + uiT('ui.admin.insecure_nodes.active', 'Active') + '</span>'
: '<span class="badge bg-secondary">' + uiT('ui.admin.insecure_nodes.inactive', 'Inactive') + '</span>'}
</td>
<td>
<button class="btn btn-sm btn-outline-primary" onclick="editNode(${node.id}, '${escapeHtml(node.address)}', '${escapeHtml(node.description || '')}', ${node.max_messages_per_session}, ${node.allow_receive}, ${node.allow_send}, ${node.is_active})">
<i class="fas fa-edit"></i>
</button>
<button class="btn btn-sm btn-outline-danger" onclick="deleteNode(${node.id}, '${escapeHtml(node.address)}')">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
`).join('');
})
.catch(error => {
console.error(uiT('ui.admin.insecure_nodes.load_nodes_failed', 'Error loading nodes') + ':', error);
document.getElementById('nodesTableBody').innerHTML =
'<tr><td colspan="8" class="text-center text-danger">' + uiT('ui.admin.insecure_nodes.load_nodes_failed', 'Error loading nodes') + '</td></tr>';
});
}
function addNode() {
const form = document.getElementById('addNodeForm');
const formData = new FormData(form);
const data = {
address: formData.get('address'),
description: formData.get('description'),
max_messages_per_session: parseInt(formData.get('max_messages_per_session')) || 100,
allow_receive: form.querySelector('[name="allow_receive"]').checked,
allow_send: form.querySelector('[name="allow_send"]').checked,
is_active: true
};
fetch('/admin/api/insecure-nodes', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
})
.then(response => response.json())
.then(result => {
if (result.success) {
alert(apiMessage(result, uiT('ui.admin.insecure_nodes.added_success', 'Node added successfully')));
bootstrap.Modal.getInstance(document.getElementById('addNodeModal')).hide();
form.reset();
loadNodes();
} else {
alert(uiT('ui.admin.insecure_nodes.error_prefix', 'Error: ') + apiError(result, uiT('ui.admin.insecure_nodes.add_failed', 'Failed to add node')));
}
})
.catch(error => {
alert(uiT('ui.admin.insecure_nodes.error_prefix', 'Error: ') + error.message);
});
}
function editNode(id, address, description, maxMessages, allowReceive, allowSend, isActive) {
document.getElementById('editNodeId').value = id;
document.getElementById('editAddress').value = address;
document.getElementById('editDescription').value = description;
document.getElementById('editMaxMessages').value = maxMessages;
document.getElementById('editAllowReceive').checked = allowReceive;
document.getElementById('editAllowSend').checked = allowSend;
document.getElementById('editIsActive').checked = isActive;
new bootstrap.Modal(document.getElementById('editNodeModal')).show();
}
function updateNode() {
const id = document.getElementById('editNodeId').value;
const form = document.getElementById('editNodeForm');
const data = {
description: document.getElementById('editDescription').value,
max_messages_per_session: parseInt(document.getElementById('editMaxMessages').value) || 100,
allow_receive: document.getElementById('editAllowReceive').checked,
allow_send: document.getElementById('editAllowSend').checked,
is_active: document.getElementById('editIsActive').checked
};
fetch(`/admin/api/insecure-nodes/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
})
.then(response => response.json())
.then(result => {
if (result.success) {
alert(apiMessage(result, uiT('ui.admin.insecure_nodes.updated_success', 'Node updated successfully')));
bootstrap.Modal.getInstance(document.getElementById('editNodeModal')).hide();
loadNodes();
} else {
alert(uiT('ui.admin.insecure_nodes.error_prefix', 'Error: ') + apiError(result, uiT('ui.admin.insecure_nodes.update_failed', 'Failed to update node')));
}
})
.catch(error => {
alert(uiT('ui.admin.insecure_nodes.error_prefix', 'Error: ') + error.message);
});
}
function deleteNode(id, address) {
if (!confirm(uiT('ui.admin.insecure_nodes.delete_confirm', `Delete ${address} from the allowlist?`, { address }))) return;
fetch(`/admin/api/insecure-nodes/${id}`, { method: 'DELETE' })
.then(response => response.json())
.then(result => {
if (result.success) {
alert(apiMessage(result, uiT('ui.admin.insecure_nodes.deleted_success', 'Node deleted successfully')));
loadNodes();
} else {
alert(uiT('ui.admin.insecure_nodes.error_prefix', 'Error: ') + apiError(result, uiT('ui.admin.insecure_nodes.delete_failed', 'Failed to delete node')));
}
})
.catch(error => {
alert(uiT('ui.admin.insecure_nodes.error_prefix', 'Error: ') + error.message);
});
}
function formatDate(dateStr) {
const date = new Date(dateStr);
return date.toLocaleString();
}
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
</script>
{% endblock %}
|