Enhance security and logging in application settings and templates; update impressum legal references

This commit is contained in:
2026-04-17 16:54:53 +02:00
parent ce20f569a3
commit b94fb1cd97
5 changed files with 167 additions and 87 deletions
+56 -68
View File
@@ -27,6 +27,7 @@ Features:
from flask import Flask, render_template, request, redirect, url_for, session, flash, send_from_directory, get_flashed_messages, jsonify, Response, make_response, send_file
from werkzeug.utils import secure_filename
from werkzeug.middleware.proxy_fix import ProxyFix
import user as us
import items as it
import ausleihung as au
@@ -44,6 +45,7 @@ import traceback
import re
import io
import html
import logging
# QR Code functionality deactivated
# import qrcode
# from qrcode.constants import ERROR_CORRECT_L
@@ -62,13 +64,34 @@ from settings import MongoClient
app = Flask(__name__, static_folder='static') # Correctly set static folder
app.logger.setLevel(logging.WARNING)
app.secret_key = cfg.SECRET_KEY
app.debug = cfg.DEBUG
app.config['UPLOAD_FOLDER'] = cfg.UPLOAD_FOLDER
app.config['THUMBNAIL_FOLDER'] = cfg.THUMBNAIL_FOLDER
app.config['PREVIEW_FOLDER'] = cfg.PREVIEW_FOLDER
app.config['ALLOWED_EXTENSIONS'] = set(cfg.ALLOWED_EXTENSIONS)
app.config['MAX_CONTENT_LENGTH'] = max(cfg.MAX_UPLOAD_MB, cfg.IMAGE_MAX_UPLOAD_MB, cfg.VIDEO_MAX_UPLOAD_MB) * 1024 * 1024
app.config['SESSION_COOKIE_HTTPONLY'] = True
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
app.config['SESSION_COOKIE_SECURE'] = cfg.SSL_ENABLED if os.getenv('INVENTAR_SESSION_COOKIE_SECURE') is None else os.getenv('INVENTAR_SESSION_COOKIE_SECURE', '').strip().lower() in ('1', 'true', 'yes', 'on')
app.config['PREFERRED_URL_SCHEME'] = 'https' if app.config['SESSION_COOKIE_SECURE'] else 'http'
# app.config['QR_CODE_FOLDER'] = cfg.QR_CODE_FOLDER # QR Code storage deactivated
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_port=1)
def print(*args, **kwargs):
if not args:
return None
message = " ".join(str(arg) for arg in args)
stripped = message.lstrip()
if stripped.startswith(('Error', 'Fehler', 'Warning', 'Warnung', '[WARN]', '[KONFLIKT]', 'Failed', 'Fehl')):
app.logger.warning(message)
elif stripped.startswith(('Exception', 'Traceback')):
app.logger.error(message)
else:
app.logger.info(message)
# Thumbnail sizes
THUMBNAIL_SIZE = cfg.THUMBNAIL_SIZE
@@ -96,6 +119,16 @@ APP_VERSION = __version__
RELEASE_STATE_FILE = os.path.join(os.path.dirname(BASE_DIR), '.release-version')
@app.after_request
def _set_security_headers(response):
response.headers.setdefault('X-Content-Type-Options', 'nosniff')
response.headers.setdefault('X-Frame-Options', 'SAMEORIGIN')
response.headers.setdefault('Referrer-Policy', 'strict-origin-when-cross-origin')
if cfg.SSL_ENABLED:
response.headers.setdefault('Strict-Transport-Security', 'max-age=31536000; includeSubDomains')
return response
def _get_asset_version():
"""Return a cache-busting asset version tied to deployment state."""
env_version = os.getenv('INVENTAR_ASSET_VERSION', '').strip()
@@ -653,14 +686,11 @@ def create_daily_backup():
Erstellt täglich ein Backup der Ausleihungsdatenbank
"""
try:
print(f"[{datetime.datetime.now()}] Erstelle Backup der Ausleihungsdatenbank...")
result = au.create_backup_database()
if result:
print(f"[{datetime.datetime.now()}] Backup erfolgreich erstellt")
else:
print(f"[{datetime.datetime.now()}] Fehler beim Erstellen des Backups")
if not result:
app.logger.warning("Daily backup creation returned false")
except Exception as e:
print(f"[{datetime.datetime.now()}] Ausnahme beim Erstellen des Backups: {str(e)}")
app.logger.error(f"Daily backup creation failed: {e}")
def update_appointment_statuses():
"""
@@ -670,17 +700,8 @@ def update_appointment_statuses():
- Aktive Termine, die beendet werden sollten
"""
current_time = datetime.datetime.now()
# Prepare logging early so it's available in exception paths
log_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'logs')
os.makedirs(log_dir, exist_ok=True)
log_file = os.path.join(log_dir, 'scheduler.log')
try:
with open(log_file, 'a', encoding='utf-8') as f:
f.write(f"[{current_time}] Starte automatische Statusaktualisierung...\n")
print(f"[{current_time}] Starte automatische Statusaktualisierung...")
# Hole alle Termine mit Status 'planned' oder 'active'
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
db = client[MONGODB_DB]
@@ -742,16 +763,15 @@ def update_appointment_statuses():
f" [KONFLIKT] Termin {appointment['_id']}: "
f"planned → active, aber {conflict_note}"
)
print(conflict_log)
with open(log_file, 'a', encoding='utf-8') as f:
f.write(f"[{current_time}] {conflict_log}\n")
app.logger.warning(conflict_log)
else:
# No conflict — clear any previously stored conflict flag
extra_fields['ConflictDetected'] = False
extra_fields['ConflictNote'] = ''
except Exception as conflict_err:
print(f" [WARN] Konfliktprüfung fehlgeschlagen für Termin "
f"{appointment['_id']}: {conflict_err}")
app.logger.warning(
f"Conflict check failed for appointment {appointment['_id']}: {conflict_err}"
)
result = ausleihungen.update_one(
{'_id': appointment['_id']},
@@ -769,37 +789,15 @@ def update_appointment_statuses():
elif new_status == 'completed':
completed_count += 1
log_msg = f" - Termin {appointment['_id']}: {old_status}{new_status}"
print(log_msg)
with open(log_file, 'a', encoding='utf-8') as f:
f.write(f"[{current_time}] {log_msg}\n")
client.close()
if updated_count > 0:
result_msg = f"Statusaktualisierung abgeschlossen: {updated_count} Termine aktualisiert"
detail_msg = f" - {activated_count} aktiviert, {completed_count} abgeschlossen"
print(f"[{current_time}] {result_msg}")
print(f"[{current_time}] {detail_msg}")
with open(log_file, 'a', encoding='utf-8') as f:
f.write(f"[{current_time}] {result_msg}\n")
f.write(f"[{current_time}] {detail_msg}\n")
else:
result_msg = "Statusaktualisierung abgeschlossen: Keine Änderungen erforderlich"
print(f"[{current_time}] {result_msg}")
with open(log_file, 'a', encoding='utf-8') as f:
f.write(f"[{current_time}] {result_msg}\n")
app.logger.warning(
f"Appointment status update finished: {updated_count} changed ({activated_count} active, {completed_count} completed)"
)
except Exception as e:
error_msg = f"Fehler bei der automatischen Statusaktualisierung: {str(e)}"
print(f"[{datetime.datetime.now()}] {error_msg}")
try:
with open(log_file, 'a', encoding='utf-8') as f:
f.write(f"[{datetime.datetime.now()}] {error_msg}\n")
except Exception:
pass
import traceback
traceback.print_exc()
app.logger.error(f"Automatic appointment status update failed: {e}")
# Schedule jobs
scheduler = BackgroundScheduler()
@@ -1484,7 +1482,7 @@ def uploaded_file(filename):
# Default placeholder from static folder
return send_from_directory(app.static_folder, 'favicon.ico')
except Exception as e:
print(f"Error serving file {filename}: {str(e)}")
app.logger.error(f"Error serving file {filename}: {str(e)}")
return Response("Image not found", status=404)
@@ -1519,7 +1517,7 @@ def thumbnail_file(filename):
else:
return send_from_directory(app.static_folder, 'favicon.ico')
except Exception as e:
print(f"Error serving thumbnail {filename}: {str(e)}")
app.logger.error(f"Error serving thumbnail {filename}: {str(e)}")
return Response("Thumbnail not found", status=404)
@@ -1554,7 +1552,7 @@ def preview_file(filename):
else:
return send_from_directory(app.static_folder, 'favicon.ico')
except Exception as e:
print(f"Error serving preview {filename}: {str(e)}")
app.logger.error(f"Error serving preview {filename}: {str(e)}")
return Response("Preview not found", status=404)
@@ -2017,7 +2015,7 @@ def api_library_items():
'has_more': (offset + count) < total_count
}), 200
except Exception as e:
print(f"Error fetching library items: {e}")
app.logger.error(f"Error fetching library items: {e}")
return jsonify({'error': str(e)}), 500
@@ -2227,7 +2225,7 @@ def api_item_detail(item_id):
"""
return detail_html, 200
except Exception as e:
print(f"Error fetching item detail: {e}")
app.logger.error(f"Error fetching item detail: {e}")
return jsonify({'error': str(e)}), 500
@@ -2330,11 +2328,6 @@ def upload_admin():
try:
original_item = it.get_item(duplicate_from)
if original_item:
# Enhanced debug logging for images
images = original_item.get('Images', [])
print(f"DEBUG: Original item: {original_item.get('_id')} has these images: {images}")
print(f"DEBUG: Images type: {type(images)}, count: {len(images) if isinstance(images, list) else 'not a list'}")
duplicate_data = {
'name': original_item.get('Name', ''),
'description': original_item.get('Beschreibung', ''),
@@ -2360,7 +2353,7 @@ def upload_admin():
else:
flash('Ursprungs-Element für Duplizierung nicht gefunden.', 'error')
except Exception as e:
print(f"Error loading item for duplication: {e}")
app.logger.warning(f"Error loading item for duplication: {e}")
flash('Fehler beim Laden der Duplizierungsdaten.', 'error')
# Handle the new method (sessionStorage-based duplication)
@@ -2429,7 +2422,7 @@ def library_admin():
else:
flash('Ursprungs-Element für Duplizierung nicht gefunden.', 'error')
except Exception as e:
print(f"Error loading item for duplication: {e}")
app.logger.warning(f"Error loading item for duplication: {e}")
flash('Fehler beim Laden der Duplizierungsdaten.', 'error')
elif duplicate_flag == 'true':
flash('Buch wird dupliziert. Die Daten werden aus dem Session-Speicher geladen.', 'info')
@@ -2486,7 +2479,7 @@ def student_cards_admin():
'notes': card.get('Notizen', '')
}
except Exception as e:
print(f"Error loading student card for edit: {e}")
app.logger.error(f"Error loading student card for edit: {e}")
flash('Fehler beim Laden des Ausweises.', 'error')
# Handle POST request (add or edit)
@@ -2504,7 +2497,7 @@ def student_cards_admin():
student_cards.delete_one({'_id': ObjectId(card_id)})
flash('Ausweis wurde gelöscht.', 'success')
except Exception as e:
print(f"Error deleting student card: {e}")
app.logger.error(f"Error deleting student card: {e}")
flash('Fehler beim Löschen des Ausweises.', 'error')
elif action == 'edit':
@@ -2532,7 +2525,7 @@ def student_cards_admin():
flash('Ausweis wurde aktualisiert.', 'success')
return redirect(url_for('student_cards_admin'))
except Exception as e:
print(f"Error updating student card: {e}")
app.logger.error(f"Error updating student card: {e}")
flash('Fehler beim Aktualisieren des Ausweises.', 'error')
elif action == 'add':
@@ -2556,7 +2549,7 @@ def student_cards_admin():
flash('Neuer Ausweis wurde hinzugefügt.', 'success')
return redirect(url_for('student_cards_admin'))
except Exception as e:
print(f"Error adding student card: {e}")
app.logger.error(f"Error adding student card: {e}")
flash('Fehler beim Hinzufügen des Ausweises.', 'error')
# Get all student cards
@@ -5196,7 +5189,7 @@ def ausleihen(id):
flash('Alle Exemplare sind aufgrund geplanter Reservierungen heute belegt.', 'error')
return redirect(url_for(redirect_target))
except Exception as e:
print(f"Warning: could not enforce planned booking guard: {e}")
app.logger.warning(f"Could not enforce planned booking guard: {e}")
# Get number of exemplars to borrow (default to 1)
exemplare_count = request.form.get('exemplare_count', 1)
@@ -5317,10 +5310,7 @@ def zurueckgeben(id):
username = session['username']
print("Code 1169: zurueckgeben called with item ID:", id)
if not item.get('Verfuegbar', True) and (us.check_admin(session['username']) or item.get('User') == username):
print("Code 1172: Item is not available, proceeding with return")
try:
# Get ALL active borrowing records for this item and complete them
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
@@ -5339,8 +5329,6 @@ def zurueckgeben(id):
updated_count = 0
for record in active_records:
ausleihung_id = str(record['_id'])
print(f"Completing active ausleihung {ausleihung_id} for item {id}")
# Update each active record
result = ausleihungen.update_one(
{'_id': ObjectId(ausleihung_id)},
+100 -12
View File
@@ -12,6 +12,8 @@ defaults for the web application and helper modules.
"""
import os
import json
import atexit
from threading import Lock
from pymongo import MongoClient as _PyMongoClient
# Base directory of this Web package
@@ -99,10 +101,27 @@ def _get(conf, path, default):
return default
return cur
def _get_bool_env(name, default):
value = os.getenv(name)
if value is None:
return default
return value.strip().lower() in ('1', 'true', 'yes', 'on')
def _get_int_env(name, default):
value = os.getenv(name)
if value is None or not value.strip():
return int(default)
try:
return int(value)
except ValueError:
return int(default)
# Expose settings
APP_VERSION = _get(_conf, ['ver'], DEFAULTS['version'])
DEBUG = _get(_conf, ['dbg'], DEFAULTS['debug'])
SECRET_KEY = str(_get(_conf, ['key'], DEFAULTS['secret_key']))
DEBUG = _get_bool_env('INVENTAR_DEBUG', _get(_conf, ['dbg'], DEFAULTS['debug']))
SECRET_KEY = str(os.getenv('INVENTAR_SECRET_KEY', _get(_conf, ['key'], DEFAULTS['secret_key'])))
HOST = _get(_conf, ['host'], DEFAULTS['host'])
PORT = _get(_conf, ['port'], DEFAULTS['port'])
@@ -115,6 +134,13 @@ MONGODB_DB = _get(_conf, ['mongodb', 'db'], DEFAULTS['mongodb']['db'])
MONGODB_HOST = os.getenv('INVENTAR_MONGODB_HOST', MONGODB_HOST)
MONGODB_PORT = int(os.getenv('INVENTAR_MONGODB_PORT', str(MONGODB_PORT)))
MONGODB_DB = os.getenv('INVENTAR_MONGODB_DB', MONGODB_DB)
MONGODB_MAX_POOL_SIZE = _get_int_env('INVENTAR_MONGODB_MAX_POOL_SIZE', 20)
MONGODB_MIN_POOL_SIZE = _get_int_env('INVENTAR_MONGODB_MIN_POOL_SIZE', 0)
MONGODB_MAX_IDLE_TIME_MS = _get_int_env('INVENTAR_MONGODB_MAX_IDLE_TIME_MS', 300000)
MONGODB_CONNECT_TIMEOUT_MS = _get_int_env('INVENTAR_MONGODB_CONNECT_TIMEOUT_MS', 5000)
MONGODB_SERVER_SELECTION_TIMEOUT_MS = _get_int_env('INVENTAR_MONGODB_SERVER_SELECTION_TIMEOUT_MS', 5000)
MONGODB_SOCKET_TIMEOUT_MS = _get_int_env('INVENTAR_MONGODB_SOCKET_TIMEOUT_MS', 30000)
MONGODB_MAX_CONNECTING = _get_int_env('INVENTAR_MONGODB_MAX_CONNECTING', 2)
# Scheduler
SCHEDULER_INTERVAL_MIN = _get(_conf, ['scheduler', 'interval_minutes'], DEFAULTS['scheduler']['interval_minutes'])
@@ -176,19 +202,81 @@ if not os.path.isabs(LOGS_FOLDER):
LOGS_FOLDER = os.path.join(PROJECT_ROOT, LOGS_FOLDER)
_MONGO_CLIENT_CACHE = {}
_MONGO_CLIENT_LOCK = Lock()
class _MongoClientProxy:
def __init__(self, client):
self._client = client
def __getattr__(self, name):
return getattr(self._client, name)
def __getitem__(self, name):
return self._client[name]
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
return False
def close(self):
return None
def _close_cached_mongo_clients():
with _MONGO_CLIENT_LOCK:
clients = list(_MONGO_CLIENT_CACHE.values())
_MONGO_CLIENT_CACHE.clear()
for proxy in clients:
try:
proxy._client.close()
except Exception:
pass
atexit.register(_close_cached_mongo_clients)
def MongoClient(*args, **kwargs):
"""Return a lightweight MongoDB client configured from this settings module."""
"""Return a process-local MongoDB client configured from this settings module."""
explicit_host = 'host' in kwargs
explicit_port = 'port' in kwargs
host = args[0] if len(args) >= 1 else kwargs.pop('host', MONGODB_HOST)
port = args[1] if len(args) >= 2 else kwargs.pop('port', MONGODB_PORT)
client_kwargs = {
'maxPoolSize': 10,
'minPoolSize': 0,
'connectTimeoutMS': 5000,
'serverSelectionTimeoutMS': 5000,
'maxPoolSize': MONGODB_MAX_POOL_SIZE,
'minPoolSize': MONGODB_MIN_POOL_SIZE,
'maxIdleTimeMS': MONGODB_MAX_IDLE_TIME_MS,
'connectTimeoutMS': MONGODB_CONNECT_TIMEOUT_MS,
'serverSelectionTimeoutMS': MONGODB_SERVER_SELECTION_TIMEOUT_MS,
'socketTimeoutMS': MONGODB_SOCKET_TIMEOUT_MS,
'maxConnecting': MONGODB_MAX_CONNECTING,
'retryWrites': True,
'retryReads': True,
}
client_kwargs.update(kwargs)
# Preserve caller-provided positional host/port arguments.
# If none are passed, use configured defaults.
if args:
return _PyMongoClient(*args, **client_kwargs)
return _PyMongoClient(MONGODB_HOST, MONGODB_PORT, **client_kwargs)
if len(args) >= 2 and not explicit_host and not explicit_port:
mongo_args = args
else:
mongo_args = (host, port)
cache_key = (
mongo_args,
tuple(sorted((key, repr(value)) for key, value in client_kwargs.items())),
)
with _MONGO_CLIENT_LOCK:
cached_client = _MONGO_CLIENT_CACHE.get(cache_key)
if cached_client is not None:
return cached_client
client = _PyMongoClient(*mongo_args, **client_kwargs)
cached_client = _MongoClientProxy(client)
_MONGO_CLIENT_CACHE[cache_key] = cached_client
return cached_client
+1 -1
View File
@@ -7,7 +7,7 @@
For commercial licensing inquiries: https://github.com/AIIrondev
-->
<!DOCTYPE html>
<html lang="en" data-module="{{ CURRENT_MODULE }}">
<html lang="de" data-module="{{ CURRENT_MODULE }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0, user-scalable=yes">
+7 -5
View File
@@ -14,18 +14,20 @@
<div class="container my-4">
<div class="impressum-content bg-white p-4 shadow rounded">
<h1 class="mb-4 text-center">Impressum</h1>
<p class="text-center mb-5">(Angaben gemäß § 5 TMG)</p>
<p class="text-center mb-5">(Angaben gemäß § 5 DDG)</p>
<div class="impressum-section mb-4">
<h3 class="mb-3">Kontaktinformationen</h3>
<p><strong>Name:</strong>Invario UG</p>
<p><strong>Adresse:</strong> Musterstraße 123<br>12345 Musterstadt<br>Deutschland</p>
<address class="mb-0">
<p><strong>Name:</strong> Invario UG</p>
<p><strong>Adresse:</strong> Musterstraße 123<br>12345 Musterstadt<br>Deutschland</p>
</address>
<p><strong>E-Mail:</strong> <a href="mailto:info@invario.eu">info@invario.eu</a></p>
</div>
<div class="impressum-section mb-4">
<h3 class="mb-3">Verantwortlich für den Inhalt</h3>
<p>(nach § 55 Abs. 2 RStV)</p>
<p>(nach § 18 Abs. 2 MStV)</p>
<p>Invario UG<br>
Musterstraße 123<br>
12345 Musterstadt<br>
@@ -36,7 +38,7 @@
<h3 class="mb-3">Haftungsausschluss</h3>
<h4 class="mb-2">Haftung für Inhalte</h4>
<p>Die Inhalte unserer Seiten wurden mit größter Sorgfalt erstellt. Für die Richtigkeit, Vollständigkeit und Aktualität der Inhalte können wir jedoch keine Gewähr übernehmen. Als Diensteanbieter sind wir gemäß § 7 Abs.1 TMG für eigene Inhalte auf diesen Seiten nach den allgemeinen Gesetzen verantwortlich. Nach §§ 8 bis 10 TMG sind wir als Diensteanbieter jedoch nicht verpflichtet, übermittelte oder gespeicherte fremde Informationen zu überwachen oder nach Umständen zu forschen, die auf eine rechtswidrige Tätigkeit hinweisen.</p>
<p>Die Inhalte unserer Seiten wurden mit größter Sorgfalt erstellt. Für die Richtigkeit, Vollständigkeit und Aktualität der Inhalte können wir jedoch keine Gewähr übernehmen. Als Diensteanbieter sind wir gemäß § 7 Abs. 1 DDG für eigene Inhalte auf diesen Seiten nach den allgemeinen Gesetzen verantwortlich. Nach §§ 8 bis 10 DDG sind wir als Diensteanbieter jedoch nicht verpflichtet, übermittelte oder gespeicherte fremde Informationen zu überwachen oder nach Umständen zu forschen, die auf eine rechtswidrige Tätigkeit hinweisen.</p>
<h4 class="mb-2">Haftung für Links</h4>
<p>Unser Angebot enthält Links zu externen Websites Dritter, auf deren Inhalte wir keinen Einfluss haben. Deshalb können wir für diese fremden Inhalte auch keine Gewähr übernehmen. Für die Inhalte der verlinkten Seiten ist stets der jeweilige Anbieter oder Betreiber der Seiten verantwortlich. Die verlinkten Seiten wurden zum Zeitpunkt der Verlinkung auf mögliche Rechtsverstöße überprüft. Rechtswidrige Inhalte waren zum Zeitpunkt der Verlinkung nicht erkennbar.</p>
+3 -1
View File
@@ -30,6 +30,8 @@ services:
dockerfile: Dockerfile
container_name: inventarsystem-app
restart: unless-stopped
security_opt:
- no-new-privileges:true
depends_on:
mongodb:
condition: service_healthy
@@ -43,7 +45,7 @@ services:
- "8000"
volumes:
- ./config.json:/app/config.json:ro
- ./Web:/app/Web
- ./Web:/app/Web:ro
- app_uploads:/app/Web/uploads
- app_thumbnails:/app/Web/thumbnails
- app_previews:/app/Web/previews