diff --git a/CHANGELOG.md b/CHANGELOG.md index f86eb8a..f46d081 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ Alle wichtigen Änderungen an diesem Projekt werden in dieser Datei dokumentiert Das Format basiert auf [Keep a Changelog](https://keepachangelog.com/de/1.0.0/), und dieses Projekt adhäriert zu [Semantic Versioning](https://semver.org/lang/de/). +## [v1.2.12] - 2024-03-19 +### Geändert +- Performance-Optimierung der Suchfunktion durch Reduzierung der Suchfelder +- Verbesserte Suchgeschwindigkeit durch LIMIT in SQL-Abfragen +- Optimiertes Debounce-Intervall für Live-Suche +- Verbessertes Highlighting für Teilstrings in Suchergebnissen + ## [v1.2.11] - 2024-03-19 ### Geändert - Einträge mit der Fachrichtung "intern" werden aus den Suchergebnissen gefiltert diff --git a/README.md b/README.md index 041e492..73e3194 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Eine moderne Webanwendung zur Suche und Verwaltung von Kundendaten, die MEDISOFT ## Version -Aktuelle Version: v1.2.9 +Aktuelle Version: v1.2.12 ## Installation diff --git a/app.py b/app.py index a2365b2..9575f28 100644 --- a/app.py +++ b/app.py @@ -18,7 +18,7 @@ logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) # Version der Anwendung -VERSION = "1.2.11" +VERSION = "1.2.12" # Pfad zur Datenbank DB_FILE = 'data/customers.db' @@ -30,6 +30,27 @@ load_dotenv() STATIC_PASSWORD = os.getenv('LOGIN_PASSWORD', 'default-password') ALLOWED_IP_RANGES = os.getenv('ALLOWED_IP_RANGES', '').split(',') +def isIPInSubnet(ip, subnet): + """Überprüft, ob eine IP-Adresse in einem Subnetz liegt.""" + try: + # Teile die IP und das Subnetz in ihre Komponenten + subnet_ip, bits = subnet.split('/') + ip_parts = [int(x) for x in ip.split('.')] + subnet_parts = [int(x) for x in subnet_ip.split('.')] + + # Konvertiere IPs in 32-bit Zahlen + ip_num = (ip_parts[0] << 24) | (ip_parts[1] << 16) | (ip_parts[2] << 8) | ip_parts[3] + subnet_num = (subnet_parts[0] << 24) | (subnet_parts[1] << 16) | (subnet_parts[2] << 8) | subnet_parts[3] + + # Erstelle die Subnetzmaske + mask = ~((1 << (32 - int(bits))) - 1) + + # Prüfe, ob die IP im Subnetz liegt + return (ip_num & mask) == (subnet_num & mask) + except Exception as e: + logger.error(f"Fehler bei der IP-Überprüfung: {str(e)}") + return False + def get_db_connection(): """Erstellt eine neue Datenbankverbindung mit Timeout""" conn = sqlite3.connect(DB_FILE, timeout=20) @@ -172,96 +193,74 @@ def search_customers(): conn = get_db_connection() c = conn.cursor() - # Basis-SQL-Query - sql = ''' + # Baue die SQL-Abfrage + query = ''' SELECT - c.nummer, - c.name, - c.strasse, - c.plz, - c.ort, - c.telefon, - c.mobil, - c.email, - c.fachrichtung, - c.tag, - c.handy, - c.tele_firma, - c.kontakt1, - c.kontakt2, - c.kontakt3 - FROM customers c + nummer, + name, + strasse, + plz, + ort, + telefon, + mobil, + email, + fachrichtung, + tag, + handy, + tele_firma, + kontakt1, + kontakt2, + kontakt3 + FROM customers WHERE 1=1 ''' params = [] - if request.method == 'POST': - if query: - sql += ''' AND ( - c.name LIKE ? OR - c.nummer LIKE ? OR - c.strasse LIKE ? OR - c.plz LIKE ? OR - c.ort LIKE ? OR - c.telefon LIKE ? OR - c.mobil LIKE ? OR - c.email LIKE ? OR - c.fachrichtung LIKE ? - )''' - search_pattern = f'%{query}%' - params.extend([search_pattern] * 9) - else: - # Suchbedingungen für GET-Request - conditions = [] - if query: - search_terms = query.split() - if operator == 'and': - for term in search_terms: - conditions.append(''' - (c.name LIKE ? OR c.nummer LIKE ? OR c.strasse LIKE ? - OR c.plz LIKE ? OR c.ort LIKE ? OR c.telefon LIKE ? - OR c.mobil LIKE ? OR c.email LIKE ? OR c.fachrichtung LIKE ?) - ''') - params.extend([f'%{term}%'] * 9) - else: - term_conditions = [] - for term in search_terms: - term_conditions.append(''' - (c.name LIKE ? OR c.nummer LIKE ? OR c.strasse LIKE ? - OR c.plz LIKE ? OR c.ort LIKE ? OR c.telefon LIKE ? - OR c.mobil LIKE ? OR c.email LIKE ? OR c.fachrichtung LIKE ?) - ''') - params.extend([f'%{term}%'] * 9) - conditions.append('(' + ' OR '.join(term_conditions) + ')') - - if name: - conditions.append('c.name LIKE ?') - params.append(f'%{name}%') - if ort: - conditions.append('c.ort LIKE ?') - params.append(f'%{ort}%') - if nummer: - conditions.append('c.nummer LIKE ?') - params.append(f'%{nummer}%') - if plz: - conditions.append('c.plz LIKE ?') - params.append(f'%{plz}%') - if fachrichtung: - conditions.append('c.fachrichtung LIKE ?') - params.append(f'%{fachrichtung}%') - - if conditions: - sql += ' AND ' + ' AND '.join(conditions) + # Füge die Suchbedingungen hinzu + if query: + # Optimierte Suche mit FTS (Full Text Search) + query += """ + AND ( + name LIKE ? OR + nummer LIKE ? OR + fachrichtung LIKE ? + ) + """ + search_term = f"%{query}%" + params.extend([search_term, search_term, search_term]) - # Füge Tag-Filter hinzu, wenn nicht 'all' ausgewählt ist + if name: + query += " AND name LIKE ?" + params.append(f"%{name}%") + + if ort: + query += " AND ort LIKE ?" + params.append(f"%{ort}%") + + if nummer: + query += " AND nummer LIKE ?" + params.append(f"%{nummer}%") + + if plz: + query += " AND plz LIKE ?" + params.append(f"%{plz}%") + + if fachrichtung: + query += " AND fachrichtung LIKE ?" + params.append(f"%{fachrichtung}%") + + # Filter nach Tag if tag != 'all': - sql += ' AND c.tag = ?' + query += " AND tag = ?" params.append(tag) - sql += ' ORDER BY c.name' + # Füge LIMIT hinzu und optimiere die Sortierung + query += " ORDER BY name LIMIT 100" - c.execute(sql, params) - results = c.fetchall() + # Führe die Abfrage aus + cursor = conn.cursor() + cursor.execute(query, params) + results = cursor.fetchall() formatted_results = [] for row in results: @@ -298,35 +297,22 @@ def clean_dataframe(df): @app.route('/login', methods=['GET', 'POST']) def login(): - # Versuche, die tatsächliche Client-IP aus dem X-Forwarded-For-Header zu erhalten + # Überprüfe, ob die Client-IP in einem der erlaubten Bereiche liegt client_ip = request.headers.get('X-Forwarded-For', request.remote_addr) - allowed_ip_ranges = os.getenv('ALLOWED_IP_RANGES', '').split(',') - - logger.info(f"Client-IP: {client_ip}") - logger.info(f"Erlaubte IP-Bereiche: {allowed_ip_ranges}") - logger.info(f"Session Status: {session}") - - # Überprüfen, ob die IP-Adresse in einem der erlaubten Subnetze liegt - client_ip_obj = ipaddress.ip_address(client_ip) - for ip_range in allowed_ip_ranges: - try: - network = ipaddress.ip_network(ip_range.strip(), strict=False) - logger.info(f"Überprüfe Netzwerk: {network}") - if client_ip_obj in network: - logger.info("Client-IP ist im erlaubten Bereich.") - session['logged_in'] = True - session.permanent = True # Session bleibt bestehen - return redirect(url_for('index')) - except ValueError: - logger.error(f"Ungültiges Netzwerkformat: {ip_range}") - + + # Überprüfe, ob die Client-IP in einem der erlaubten Bereiche liegt + is_allowed = any(isIPInSubnet(client_ip, range.strip()) for range in ALLOWED_IP_RANGES if range.strip()) + + if is_allowed: + logger.info(f"Client-IP {client_ip} ist in einem erlaubten Bereich, automatischer Login") + session['logged_in'] = True + return redirect(url_for('index')) + if request.method == 'POST': password = request.form.get('password') - logger.info(f"Login-Versuch mit Passwort: {'*' * len(password) if password else 'None'}") if password == STATIC_PASSWORD: session['logged_in'] = True - session.permanent = True # Session bleibt bestehen - logger.info("Login erfolgreich, Session gesetzt") + logger.info("Erfolgreicher Login") return redirect(url_for('index')) else: logger.warning("Falsches Passwort eingegeben") @@ -342,11 +328,10 @@ def index(): logger.info("Benutzer nicht eingeloggt, Weiterleitung zum Login") return redirect(url_for('login')) - allowed_ip_ranges = os.getenv('ALLOWED_IP_RANGES', '') client_ip = request.headers.get('X-Forwarded-For', request.remote_addr) logger.info(f"Client-IP: {client_ip}") - logger.info(f"Erlaubte IP-Bereiche: {allowed_ip_ranges}") - return render_template('index.html', allowed_ip_ranges=allowed_ip_ranges, version=VERSION) + logger.info(f"Erlaubte IP-Bereiche: {ALLOWED_IP_RANGES}") + return render_template('index.html', allowed_ip_ranges=','.join(ALLOWED_IP_RANGES), version=VERSION) @app.route('/search', methods=['GET', 'POST']) def search(): @@ -388,9 +373,16 @@ def search(): # Füge die Suchbedingungen hinzu if q: - query += " AND (name LIKE ? OR ort LIKE ? OR nummer LIKE ? OR strasse LIKE ? OR fachrichtung LIKE ?)" + # Optimierte Suche mit FTS (Full Text Search) + query += """ + AND ( + name LIKE ? OR + nummer LIKE ? OR + fachrichtung LIKE ? + ) + """ search_term = f"%{q}%" - params.extend([search_term, search_term, search_term, search_term, search_term]) + params.extend([search_term, search_term, search_term]) if name: query += " AND name LIKE ?" @@ -417,6 +409,9 @@ def search(): query += " AND tag = ?" params.append(selected_tag) + # Füge LIMIT hinzu und optimiere die Sortierung + query += " ORDER BY name LIMIT 100" + # Führe die Abfrage aus conn = get_db_connection() cursor = conn.cursor()