diff --git a/links/mini_apps_views.py b/links/mini_apps_views.py index 21c092d..594fe86 100644 --- a/links/mini_apps_views.py +++ b/links/mini_apps_views.py @@ -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) diff --git a/links/templates/links/mini_apps/ip_ban.html b/links/templates/links/mini_apps/ip_ban.html new file mode 100644 index 0000000..9ab3367 --- /dev/null +++ b/links/templates/links/mini_apps/ip_ban.html @@ -0,0 +1,190 @@ +{% extends 'base.html' %} +{% load i18n %} + +{% block title %}Nginx IP Ban Manager{% endblock %} + +{% block content %} +
+ Manages block-cidrs-manual
+ in the ingress-nginx-controller ConfigMap
+
+ Enter one or more IPs or CIDRs, comma- or newline-separated.
+ Examples: 1.2.3.4,
+ 5.6.7.0/24
+
+ No IPs are currently banned. +
+ {% endif %} +