2 Commits

4 changed files with 114 additions and 124 deletions

View File

@@ -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/), 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/). 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 ## [v1.2.11] - 2024-03-19
### Geändert ### Geändert
- Einträge mit der Fachrichtung "intern" werden aus den Suchergebnissen gefiltert - Einträge mit der Fachrichtung "intern" werden aus den Suchergebnissen gefiltert

View File

@@ -14,7 +14,7 @@ Eine moderne Webanwendung zur Suche und Verwaltung von Kundendaten, die MEDISOFT
## Version ## Version
Aktuelle Version: v1.2.10 Aktuelle Version: v1.2.12
## Installation ## Installation

215
app.py
View File

@@ -18,7 +18,7 @@ logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Version der Anwendung # Version der Anwendung
VERSION = "1.2.11" VERSION = "1.2.12"
# Pfad zur Datenbank # Pfad zur Datenbank
DB_FILE = 'data/customers.db' DB_FILE = 'data/customers.db'
@@ -30,6 +30,27 @@ load_dotenv()
STATIC_PASSWORD = os.getenv('LOGIN_PASSWORD', 'default-password') STATIC_PASSWORD = os.getenv('LOGIN_PASSWORD', 'default-password')
ALLOWED_IP_RANGES = os.getenv('ALLOWED_IP_RANGES', '').split(',') 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(): def get_db_connection():
"""Erstellt eine neue Datenbankverbindung mit Timeout""" """Erstellt eine neue Datenbankverbindung mit Timeout"""
conn = sqlite3.connect(DB_FILE, timeout=20) conn = sqlite3.connect(DB_FILE, timeout=20)
@@ -172,96 +193,74 @@ def search_customers():
conn = get_db_connection() conn = get_db_connection()
c = conn.cursor() c = conn.cursor()
# Basis-SQL-Query # Baue die SQL-Abfrage
sql = ''' query = '''
SELECT SELECT
c.nummer, nummer,
c.name, name,
c.strasse, strasse,
c.plz, plz,
c.ort, ort,
c.telefon, telefon,
c.mobil, mobil,
c.email, email,
c.fachrichtung, fachrichtung,
c.tag, tag,
c.handy, handy,
c.tele_firma, tele_firma,
c.kontakt1, kontakt1,
c.kontakt2, kontakt2,
c.kontakt3 kontakt3
FROM customers c FROM customers
WHERE 1=1 WHERE 1=1
''' '''
params = [] params = []
if request.method == 'POST': # Füge die Suchbedingungen hinzu
if query: if query:
sql += ''' AND ( # Optimierte Suche mit FTS (Full Text Search)
c.name LIKE ? OR query += """
c.nummer LIKE ? OR AND (
c.strasse LIKE ? OR name LIKE ? OR
c.plz LIKE ? OR nummer LIKE ? OR
c.ort LIKE ? OR fachrichtung LIKE ?
c.telefon LIKE ? OR )
c.mobil LIKE ? OR """
c.email LIKE ? OR search_term = f"%{query}%"
c.fachrichtung LIKE ? params.extend([search_term, search_term, search_term])
)'''
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 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': if tag != 'all':
sql += ' AND c.tag = ?' query += " AND tag = ?"
params.append(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) # Führe die Abfrage aus
results = c.fetchall() cursor = conn.cursor()
cursor.execute(query, params)
results = cursor.fetchall()
formatted_results = [] formatted_results = []
for row in results: for row in results:
@@ -298,35 +297,22 @@ def clean_dataframe(df):
@app.route('/login', methods=['GET', 'POST']) @app.route('/login', methods=['GET', 'POST'])
def login(): 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) client_ip = request.headers.get('X-Forwarded-For', request.remote_addr)
allowed_ip_ranges = os.getenv('ALLOWED_IP_RANGES', '').split(',')
# Überprüfe, ob die Client-IP in einem der erlaubten Bereiche liegt
logger.info(f"Client-IP: {client_ip}") is_allowed = any(isIPInSubnet(client_ip, range.strip()) for range in ALLOWED_IP_RANGES if range.strip())
logger.info(f"Erlaubte IP-Bereiche: {allowed_ip_ranges}")
logger.info(f"Session Status: {session}") if is_allowed:
logger.info(f"Client-IP {client_ip} ist in einem erlaubten Bereich, automatischer Login")
# Überprüfen, ob die IP-Adresse in einem der erlaubten Subnetze liegt session['logged_in'] = True
client_ip_obj = ipaddress.ip_address(client_ip) return redirect(url_for('index'))
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}")
if request.method == 'POST': if request.method == 'POST':
password = request.form.get('password') password = request.form.get('password')
logger.info(f"Login-Versuch mit Passwort: {'*' * len(password) if password else 'None'}")
if password == STATIC_PASSWORD: if password == STATIC_PASSWORD:
session['logged_in'] = True session['logged_in'] = True
session.permanent = True # Session bleibt bestehen logger.info("Erfolgreicher Login")
logger.info("Login erfolgreich, Session gesetzt")
return redirect(url_for('index')) return redirect(url_for('index'))
else: else:
logger.warning("Falsches Passwort eingegeben") logger.warning("Falsches Passwort eingegeben")
@@ -342,11 +328,10 @@ def index():
logger.info("Benutzer nicht eingeloggt, Weiterleitung zum Login") logger.info("Benutzer nicht eingeloggt, Weiterleitung zum Login")
return redirect(url_for('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) client_ip = request.headers.get('X-Forwarded-For', request.remote_addr)
logger.info(f"Client-IP: {client_ip}") logger.info(f"Client-IP: {client_ip}")
logger.info(f"Erlaubte IP-Bereiche: {allowed_ip_ranges}") logger.info(f"Erlaubte IP-Bereiche: {ALLOWED_IP_RANGES}")
return render_template('index.html', allowed_ip_ranges=allowed_ip_ranges, version=VERSION) return render_template('index.html', allowed_ip_ranges=','.join(ALLOWED_IP_RANGES), version=VERSION)
@app.route('/search', methods=['GET', 'POST']) @app.route('/search', methods=['GET', 'POST'])
def search(): def search():
@@ -388,9 +373,16 @@ def search():
# Füge die Suchbedingungen hinzu # Füge die Suchbedingungen hinzu
if q: 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}%" 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: if name:
query += " AND name LIKE ?" query += " AND name LIKE ?"
@@ -417,6 +409,9 @@ def search():
query += " AND tag = ?" query += " AND tag = ?"
params.append(selected_tag) params.append(selected_tag)
# Füge LIMIT hinzu und optimiere die Sortierung
query += " ORDER BY name LIMIT 100"
# Führe die Abfrage aus # Führe die Abfrage aus
conn = get_db_connection() conn = get_db_connection()
cursor = conn.cursor() cursor = conn.cursor()

View File

@@ -24,16 +24,6 @@
<i class="fas fa-times reset-icon" onclick="clearInput('q')"></i> <i class="fas fa-times reset-icon" onclick="clearInput('q')"></i>
<i class="fas fa-search search-icon"></i> <i class="fas fa-search search-icon"></i>
</div> </div>
<div class="search-options mt-2">
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="searchOperator" id="searchOr" value="or" checked>
<label class="form-check-label" for="searchOr">ODER</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="searchOperator" id="searchAnd" value="and">
<label class="form-check-label" for="searchAnd">UND</label>
</div>
</div>
</div> </div>
<div class="search-fields"> <div class="search-fields">
@@ -265,7 +255,7 @@
<div class="d-flex justify-content-between align-items-start"> <div class="d-flex justify-content-between align-items-start">
<h5 class="card-title mb-1">${customer.name}</h5> <h5 class="card-title mb-1">${customer.name}</h5>
<div class="d-flex align-items-center gap-2"> <div class="d-flex align-items-center gap-2">
<span class="badge ${customer.tag === 'medisoft' ? 'bg-primary' : 'bg-warning text-dark'}">${customer.tag.toUpperCase()}</span> <span class="badge ${(customer.tag || 'medisoft') === 'medisoft' ? 'bg-primary' : 'bg-warning text-dark'}">${(customer.tag || 'medisoft').toUpperCase()}</span>
<button class="btn btn-sm btn-outline-primary" onclick="copyCustomerLink('${customer.nummer}')"> <button class="btn btn-sm btn-outline-primary" onclick="copyCustomerLink('${customer.nummer}')">
<i class="fas fa-share-alt"></i> Teilen <i class="fas fa-share-alt"></i> Teilen
</button> </button>
@@ -304,7 +294,6 @@
const nummer = document.getElementById('nummerInput').value; const nummer = document.getElementById('nummerInput').value;
const plz = document.getElementById('plzInput').value; const plz = document.getElementById('plzInput').value;
const fachrichtung = document.getElementById('fachrichtungInput').value; const fachrichtung = document.getElementById('fachrichtungInput').value;
const searchOperator = document.querySelector('input[name="searchOperator"]:checked').value;
const selectedTag = document.getElementById('tagFilter').value; const selectedTag = document.getElementById('tagFilter').value;
// Zeige das Lade-Icon // Zeige das Lade-Icon
@@ -318,7 +307,6 @@
if (nummer) params.append('nummer', nummer); if (nummer) params.append('nummer', nummer);
if (plz) params.append('plz', plz); if (plz) params.append('plz', plz);
if (fachrichtung) params.append('fachrichtung', fachrichtung); if (fachrichtung) params.append('fachrichtung', fachrichtung);
if (searchOperator) params.append('operator', searchOperator);
if (selectedTag) params.append('tag', selectedTag); if (selectedTag) params.append('tag', selectedTag);
// Führe die Suche durch // Führe die Suche durch