Compare commits
2 Commits
Author | SHA1 | Date | |
---|---|---|---|
c1b55c3579 | |||
6a2e290d54 |
@@ -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
|
||||||
|
@@ -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
215
app.py
@@ -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()
|
||||||
|
@@ -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
|
||||||
|
Reference in New Issue
Block a user