mirror of
https://github.com/wahyd4/links.git
synced 2026-08-09 05:06:16 +10:00
Update
This commit is contained in:
@@ -1,5 +1,11 @@
|
||||
from django.views.generic import TemplateView
|
||||
from django.shortcuts import render
|
||||
from django.http import JsonResponse
|
||||
from django.views import View
|
||||
import json
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MiniAppsListView(TemplateView):
|
||||
@@ -34,6 +40,14 @@ class MiniAppsListView(TemplateView):
|
||||
'icon': 'fas fa-chart-line',
|
||||
'color': '#27ae60'
|
||||
},
|
||||
{
|
||||
'name': 'Nginx IP Ban',
|
||||
'description': 'Manage the nginx ingress block list — view, add, and remove banned IPs and CIDR ranges directly from the Kubernetes ConfigMap.',
|
||||
'url': 'mini-apps-ip-ban',
|
||||
'thumbnail': 'https://images.unsplash.com/photo-1614064641938-3bbee52942c7?w=400&h=300&fit=crop',
|
||||
'icon': 'fas fa-ban',
|
||||
'color': '#e74c3c'
|
||||
},
|
||||
{
|
||||
'name': 'Weather Dashboard',
|
||||
'description': 'Real-time weather information with beautiful visualizations and forecasts.',
|
||||
@@ -73,3 +87,84 @@ class FIREPlanningView(TemplateView):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['page_title'] = 'FIRE Planning Calculator'
|
||||
return context
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Nginx IP Ban mini app
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_CONFIGMAP_NAME = 'ingress-nginx-controller'
|
||||
_CONFIGMAP_NS = 'ingress-nginx'
|
||||
_CONFIGMAP_KEY = 'block-cidrs-manual'
|
||||
|
||||
|
||||
def _k8s_v1():
|
||||
"""Return a CoreV1Api client, preferring in-cluster then local kubeconfig."""
|
||||
from kubernetes import client, config
|
||||
try:
|
||||
config.load_incluster_config()
|
||||
except config.ConfigException:
|
||||
config.load_kube_config()
|
||||
return client.CoreV1Api()
|
||||
|
||||
|
||||
def _read_blocked_ips() -> list[str]:
|
||||
v1 = _k8s_v1()
|
||||
cm = v1.read_namespaced_config_map(_CONFIGMAP_NAME, _CONFIGMAP_NS)
|
||||
raw = (cm.data or {}).get(_CONFIGMAP_KEY, '')
|
||||
return [ip.strip() for ip in raw.split(',') if ip.strip()]
|
||||
|
||||
|
||||
def _write_blocked_ips(ips: list[str]) -> None:
|
||||
v1 = _k8s_v1()
|
||||
from kubernetes.client import V1ConfigMap
|
||||
body = V1ConfigMap(data={_CONFIGMAP_KEY: ','.join(ips)})
|
||||
v1.patch_namespaced_config_map(_CONFIGMAP_NAME, _CONFIGMAP_NS, body)
|
||||
|
||||
|
||||
class NginxIPBanView(View):
|
||||
template = 'links/mini_apps/ip_ban.html'
|
||||
|
||||
def get(self, request):
|
||||
error = None
|
||||
blocked = []
|
||||
try:
|
||||
blocked = _read_blocked_ips()
|
||||
except Exception as exc:
|
||||
logger.error('ip_ban: failed to read ConfigMap: %s', exc)
|
||||
error = str(exc)
|
||||
return render(request, self.template, {'blocked': blocked, 'error': error})
|
||||
|
||||
def post(self, request):
|
||||
action = request.POST.get('action')
|
||||
try:
|
||||
blocked = _read_blocked_ips()
|
||||
if action == 'add':
|
||||
raw = request.POST.get('ips', '')
|
||||
# Accept comma- or newline-separated entries
|
||||
new_ips = [
|
||||
ip.strip()
|
||||
for part in raw.replace('\n', ',').split(',')
|
||||
for ip in [part.strip()]
|
||||
if ip
|
||||
]
|
||||
added = 0
|
||||
for ip in new_ips:
|
||||
if ip not in blocked:
|
||||
blocked.append(ip)
|
||||
added += 1
|
||||
_write_blocked_ips(blocked)
|
||||
return JsonResponse({'ok': True, 'blocked': blocked, 'added': added})
|
||||
|
||||
elif action == 'remove':
|
||||
ip = request.POST.get('ip', '').strip()
|
||||
if ip in blocked:
|
||||
blocked.remove(ip)
|
||||
_write_blocked_ips(blocked)
|
||||
return JsonResponse({'ok': True, 'blocked': blocked})
|
||||
|
||||
return JsonResponse({'ok': False, 'error': 'Unknown action'}, status=400)
|
||||
|
||||
except Exception as exc:
|
||||
logger.error('ip_ban: action=%s error=%s', action, exc)
|
||||
return JsonResponse({'ok': False, 'error': str(exc)}, status=500)
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}Nginx IP Ban Manager{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-8 space-y-6">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="flex items-center gap-3">
|
||||
<a href="{% url 'mini-apps-list' %}" class="text-gray-400 hover:text-gray-600">
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
</a>
|
||||
<div>
|
||||
<h1 class="text-xl font-bold text-gray-900 flex items-center gap-2">
|
||||
<i class="fas fa-ban text-red-500"></i> Nginx IP Ban Manager
|
||||
</h1>
|
||||
<p class="text-sm text-gray-400 mt-0.5">
|
||||
Manages <code class="bg-gray-100 px-1 rounded text-xs">block-cidrs-manual</code>
|
||||
in the <code class="bg-gray-100 px-1 rounded text-xs">ingress-nginx-controller</code> ConfigMap
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Error banner -->
|
||||
{% if error %}
|
||||
<div id="error-banner" class="px-4 py-3 rounded-md text-sm border bg-red-50 text-red-800 border-red-200">
|
||||
<i class="fas fa-exclamation-triangle mr-2"></i>{{ error }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- AJAX feedback -->
|
||||
<div id="feedback" class="hidden px-4 py-2 rounded-md text-sm border"></div>
|
||||
|
||||
<!-- Add IPs -->
|
||||
<div class="bg-white shadow-sm rounded-lg border border-gray-200 overflow-hidden">
|
||||
<div class="px-6 py-4 bg-gray-50 border-b border-gray-200">
|
||||
<h2 class="text-sm font-semibold text-gray-700">
|
||||
<i class="fas fa-plus-circle text-red-500 mr-1"></i> Ban IP(s) / CIDR(s)
|
||||
</h2>
|
||||
<p class="text-xs text-gray-400 mt-0.5">
|
||||
Enter one or more IPs or CIDRs, comma- or newline-separated.
|
||||
Examples: <code class="bg-gray-100 px-1 rounded">1.2.3.4</code>,
|
||||
<code class="bg-gray-100 px-1 rounded">5.6.7.0/24</code>
|
||||
</p>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<textarea id="add-ips-input" rows="3"
|
||||
placeholder="1.2.3.4 5.6.7.0/24 8.8.8.8, 9.9.9.9"
|
||||
class="w-full border border-gray-300 rounded-md px-3 py-2 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-red-400 focus:border-transparent"></textarea>
|
||||
<div class="mt-3 flex items-center gap-3">
|
||||
<button id="add-btn"
|
||||
class="inline-flex items-center px-5 py-2 bg-red-600 text-white text-sm font-medium rounded-md hover:bg-red-700 transition-colors">
|
||||
<i class="fas fa-ban mr-2"></i> Ban IPs
|
||||
</button>
|
||||
<span class="text-xs text-gray-400">Duplicates are skipped automatically.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Banned IPs list -->
|
||||
<div class="bg-white shadow-sm rounded-lg border border-gray-200 overflow-hidden">
|
||||
<div class="px-6 py-4 bg-gray-50 border-b border-gray-200 flex items-center justify-between">
|
||||
<h2 class="text-sm font-semibold text-gray-700">
|
||||
<i class="fas fa-list text-gray-500 mr-1"></i>
|
||||
Currently Banned
|
||||
<span id="count-badge"
|
||||
class="ml-2 inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-700">
|
||||
{{ blocked|length }}
|
||||
</span>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div id="ip-list">
|
||||
{% if blocked %}
|
||||
<ul class="divide-y divide-gray-100">
|
||||
{% for ip in blocked %}
|
||||
<li class="flex items-center justify-between px-6 py-3 hover:bg-gray-50" data-ip="{{ ip }}">
|
||||
<span class="font-mono text-sm text-gray-800">{{ ip }}</span>
|
||||
<button class="remove-btn text-xs text-red-500 hover:text-red-700 font-medium transition-colors"
|
||||
data-ip="{{ ip }}">
|
||||
<i class="fas fa-times mr-1"></i>Remove
|
||||
</button>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p id="empty-msg" class="px-6 py-8 text-sm text-gray-400 text-center">
|
||||
<i class="fas fa-check-circle text-green-400 mr-2"></i>No IPs are currently banned.
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const csrfToken = '{{ csrf_token }}';
|
||||
const feedbackEl = document.getElementById('feedback');
|
||||
const listEl = document.getElementById('ip-list');
|
||||
const countBadge = document.getElementById('count-badge');
|
||||
|
||||
function showFeedback(msg, isError) {
|
||||
feedbackEl.textContent = msg;
|
||||
feedbackEl.className = isError
|
||||
? 'px-4 py-2 rounded-md text-sm border bg-red-50 text-red-800 border-red-200'
|
||||
: 'px-4 py-2 rounded-md text-sm border bg-green-50 text-green-800 border-green-200';
|
||||
feedbackEl.classList.remove('hidden');
|
||||
clearTimeout(feedbackEl._t);
|
||||
feedbackEl._t = setTimeout(() => feedbackEl.classList.add('hidden'), 4000);
|
||||
}
|
||||
|
||||
function renderList(blocked) {
|
||||
countBadge.textContent = blocked.length;
|
||||
if (blocked.length === 0) {
|
||||
listEl.innerHTML = '<p id="empty-msg" class="px-6 py-8 text-sm text-gray-400 text-center"><i class="fas fa-check-circle text-green-400 mr-2"></i>No IPs are currently banned.</p>';
|
||||
return;
|
||||
}
|
||||
const rows = blocked.map(ip =>
|
||||
`<li class="flex items-center justify-between px-6 py-3 hover:bg-gray-50" data-ip="${escHtml(ip)}">
|
||||
<span class="font-mono text-sm text-gray-800">${escHtml(ip)}</span>
|
||||
<button class="remove-btn text-xs text-red-500 hover:text-red-700 font-medium transition-colors" data-ip="${escHtml(ip)}">
|
||||
<i class="fas fa-times mr-1"></i>Remove
|
||||
</button>
|
||||
</li>`
|
||||
).join('');
|
||||
listEl.innerHTML = `<ul class="divide-y divide-gray-100">${rows}</ul>`;
|
||||
listEl.querySelectorAll('.remove-btn').forEach(btn => btn.addEventListener('click', handleRemove));
|
||||
}
|
||||
|
||||
function escHtml(str) {
|
||||
return str.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
|
||||
async function post(data) {
|
||||
const body = new URLSearchParams({csrfmiddlewaretoken: csrfToken, ...data});
|
||||
const resp = await fetch(window.location.pathname, {method: 'POST', body});
|
||||
return resp.json();
|
||||
}
|
||||
|
||||
// Add
|
||||
document.getElementById('add-btn').addEventListener('click', async () => {
|
||||
const input = document.getElementById('add-ips-input');
|
||||
const val = input.value.trim();
|
||||
if (!val) return;
|
||||
try {
|
||||
const data = await post({action: 'add', ips: val});
|
||||
if (data.ok) {
|
||||
input.value = '';
|
||||
renderList(data.blocked);
|
||||
showFeedback(`✓ Banned ${data.added} new IP(s). Total: ${data.blocked.length}.`, false);
|
||||
} else {
|
||||
showFeedback('Error: ' + data.error, true);
|
||||
}
|
||||
} catch (e) {
|
||||
showFeedback('Network error: ' + e, true);
|
||||
}
|
||||
});
|
||||
|
||||
// Remove (delegated for dynamically rendered rows)
|
||||
listEl.addEventListener('click', handleRemoveDelegated);
|
||||
|
||||
async function handleRemoveDelegated(e) {
|
||||
const btn = e.target.closest('.remove-btn');
|
||||
if (!btn) return;
|
||||
await handleRemove.call(btn, e);
|
||||
}
|
||||
|
||||
async function handleRemove(e) {
|
||||
const ip = this.dataset.ip;
|
||||
if (!confirm(`Remove "${ip}" from the ban list?`)) return;
|
||||
try {
|
||||
const data = await post({action: 'remove', ip});
|
||||
if (data.ok) {
|
||||
renderList(data.blocked);
|
||||
showFeedback(`✓ Removed ${ip}.`, false);
|
||||
} else {
|
||||
showFeedback('Error: ' + data.error, true);
|
||||
}
|
||||
} catch (e) {
|
||||
showFeedback('Network error: ' + e, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Wire up initial remove buttons
|
||||
listEl.querySelectorAll('.remove-btn').forEach(btn => btn.addEventListener('click', handleRemove));
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -56,6 +56,7 @@ urlpatterns = [
|
||||
path('ui/mini-apps/image-gallery/', mini_apps_views.ImageGalleryView.as_view(), name='mini-apps-image-gallery'),
|
||||
path('ui/mini-apps/tts/', mini_apps_views.TTSView.as_view(), name='mini-apps-tts'),
|
||||
path('ui/mini-apps/fire-planning/', mini_apps_views.FIREPlanningView.as_view(), name='mini-apps-fire-planning'),
|
||||
path('ui/mini-apps/ip-ban/', mini_apps_views.NginxIPBanView.as_view(), name='mini-apps-ip-ban'),
|
||||
|
||||
# TTS API
|
||||
path('ui/tts-api/', views.generate_tts_api, name='tts-api'),
|
||||
|
||||
Reference in New Issue
Block a user