Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5d8213b8c2 | |||
| b94fb1cd97 | |||
| ce20f569a3 |
+138
-77
@@ -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 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.utils import secure_filename
|
||||||
|
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||||
import user as us
|
import user as us
|
||||||
import items as it
|
import items as it
|
||||||
import ausleihung as au
|
import ausleihung as au
|
||||||
@@ -44,6 +45,7 @@ import traceback
|
|||||||
import re
|
import re
|
||||||
import io
|
import io
|
||||||
import html
|
import html
|
||||||
|
import logging
|
||||||
# QR Code functionality deactivated
|
# QR Code functionality deactivated
|
||||||
# import qrcode
|
# import qrcode
|
||||||
# from qrcode.constants import ERROR_CORRECT_L
|
# from qrcode.constants import ERROR_CORRECT_L
|
||||||
@@ -54,6 +56,11 @@ import uuid
|
|||||||
from PIL import Image, ImageOps
|
from PIL import Image, ImageOps
|
||||||
import mimetypes
|
import mimetypes
|
||||||
import subprocess
|
import subprocess
|
||||||
|
from data_protection import (
|
||||||
|
decrypt_document_fields,
|
||||||
|
encrypt_document_fields,
|
||||||
|
encrypt_soft_deleted_media_pack,
|
||||||
|
)
|
||||||
|
|
||||||
# Set base directory and centralized settings
|
# Set base directory and centralized settings
|
||||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
@@ -62,13 +69,43 @@ from settings import MongoClient
|
|||||||
|
|
||||||
|
|
||||||
app = Flask(__name__, static_folder='static') # Correctly set static folder
|
app = Flask(__name__, static_folder='static') # Correctly set static folder
|
||||||
|
app.logger.setLevel(logging.WARNING)
|
||||||
app.secret_key = cfg.SECRET_KEY
|
app.secret_key = cfg.SECRET_KEY
|
||||||
app.debug = cfg.DEBUG
|
app.debug = cfg.DEBUG
|
||||||
app.config['UPLOAD_FOLDER'] = cfg.UPLOAD_FOLDER
|
app.config['UPLOAD_FOLDER'] = cfg.UPLOAD_FOLDER
|
||||||
app.config['THUMBNAIL_FOLDER'] = cfg.THUMBNAIL_FOLDER
|
app.config['THUMBNAIL_FOLDER'] = cfg.THUMBNAIL_FOLDER
|
||||||
app.config['PREVIEW_FOLDER'] = cfg.PREVIEW_FOLDER
|
app.config['PREVIEW_FOLDER'] = cfg.PREVIEW_FOLDER
|
||||||
app.config['ALLOWED_EXTENSIONS'] = set(cfg.ALLOWED_EXTENSIONS)
|
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.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)
|
||||||
|
|
||||||
|
|
||||||
|
STUDENT_CARD_ENCRYPTED_FIELDS = ('SchülerName', 'Klasse', 'Notizen')
|
||||||
|
|
||||||
|
|
||||||
|
def _decrypt_student_card_doc(card_doc):
|
||||||
|
if not card_doc:
|
||||||
|
return card_doc
|
||||||
|
return decrypt_document_fields(card_doc, STUDENT_CARD_ENCRYPTED_FIELDS)
|
||||||
|
|
||||||
# Thumbnail sizes
|
# Thumbnail sizes
|
||||||
THUMBNAIL_SIZE = cfg.THUMBNAIL_SIZE
|
THUMBNAIL_SIZE = cfg.THUMBNAIL_SIZE
|
||||||
@@ -96,6 +133,16 @@ APP_VERSION = __version__
|
|||||||
RELEASE_STATE_FILE = os.path.join(os.path.dirname(BASE_DIR), '.release-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():
|
def _get_asset_version():
|
||||||
"""Return a cache-busting asset version tied to deployment state."""
|
"""Return a cache-busting asset version tied to deployment state."""
|
||||||
env_version = os.getenv('INVENTAR_ASSET_VERSION', '').strip()
|
env_version = os.getenv('INVENTAR_ASSET_VERSION', '').strip()
|
||||||
@@ -653,14 +700,11 @@ def create_daily_backup():
|
|||||||
Erstellt täglich ein Backup der Ausleihungsdatenbank
|
Erstellt täglich ein Backup der Ausleihungsdatenbank
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
print(f"[{datetime.datetime.now()}] Erstelle Backup der Ausleihungsdatenbank...")
|
|
||||||
result = au.create_backup_database()
|
result = au.create_backup_database()
|
||||||
if result:
|
if not result:
|
||||||
print(f"[{datetime.datetime.now()}] Backup erfolgreich erstellt")
|
app.logger.warning("Daily backup creation returned false")
|
||||||
else:
|
|
||||||
print(f"[{datetime.datetime.now()}] Fehler beim Erstellen des Backups")
|
|
||||||
except Exception as e:
|
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():
|
def update_appointment_statuses():
|
||||||
"""
|
"""
|
||||||
@@ -670,17 +714,8 @@ def update_appointment_statuses():
|
|||||||
- Aktive Termine, die beendet werden sollten
|
- Aktive Termine, die beendet werden sollten
|
||||||
"""
|
"""
|
||||||
current_time = datetime.datetime.now()
|
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:
|
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'
|
# Hole alle Termine mit Status 'planned' oder 'active'
|
||||||
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
|
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
|
||||||
db = client[MONGODB_DB]
|
db = client[MONGODB_DB]
|
||||||
@@ -742,16 +777,15 @@ def update_appointment_statuses():
|
|||||||
f" [KONFLIKT] Termin {appointment['_id']}: "
|
f" [KONFLIKT] Termin {appointment['_id']}: "
|
||||||
f"planned → active, aber {conflict_note}"
|
f"planned → active, aber {conflict_note}"
|
||||||
)
|
)
|
||||||
print(conflict_log)
|
app.logger.warning(conflict_log)
|
||||||
with open(log_file, 'a', encoding='utf-8') as f:
|
|
||||||
f.write(f"[{current_time}] {conflict_log}\n")
|
|
||||||
else:
|
else:
|
||||||
# No conflict — clear any previously stored conflict flag
|
# No conflict — clear any previously stored conflict flag
|
||||||
extra_fields['ConflictDetected'] = False
|
extra_fields['ConflictDetected'] = False
|
||||||
extra_fields['ConflictNote'] = ''
|
extra_fields['ConflictNote'] = ''
|
||||||
except Exception as conflict_err:
|
except Exception as conflict_err:
|
||||||
print(f" [WARN] Konfliktprüfung fehlgeschlagen für Termin "
|
app.logger.warning(
|
||||||
f"{appointment['_id']}: {conflict_err}")
|
f"Conflict check failed for appointment {appointment['_id']}: {conflict_err}"
|
||||||
|
)
|
||||||
|
|
||||||
result = ausleihungen.update_one(
|
result = ausleihungen.update_one(
|
||||||
{'_id': appointment['_id']},
|
{'_id': appointment['_id']},
|
||||||
@@ -769,37 +803,15 @@ def update_appointment_statuses():
|
|||||||
elif new_status == 'completed':
|
elif new_status == 'completed':
|
||||||
completed_count += 1
|
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()
|
client.close()
|
||||||
|
|
||||||
if updated_count > 0:
|
if updated_count > 0:
|
||||||
result_msg = f"Statusaktualisierung abgeschlossen: {updated_count} Termine aktualisiert"
|
app.logger.warning(
|
||||||
detail_msg = f" - {activated_count} aktiviert, {completed_count} abgeschlossen"
|
f"Appointment status update finished: {updated_count} changed ({activated_count} active, {completed_count} completed)"
|
||||||
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")
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = f"Fehler bei der automatischen Statusaktualisierung: {str(e)}"
|
app.logger.error(f"Automatic appointment status update failed: {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()
|
|
||||||
|
|
||||||
# Schedule jobs
|
# Schedule jobs
|
||||||
scheduler = BackgroundScheduler()
|
scheduler = BackgroundScheduler()
|
||||||
@@ -1484,7 +1496,7 @@ def uploaded_file(filename):
|
|||||||
# Default placeholder from static folder
|
# Default placeholder from static folder
|
||||||
return send_from_directory(app.static_folder, 'favicon.ico')
|
return send_from_directory(app.static_folder, 'favicon.ico')
|
||||||
except Exception as e:
|
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)
|
return Response("Image not found", status=404)
|
||||||
|
|
||||||
|
|
||||||
@@ -1519,7 +1531,7 @@ def thumbnail_file(filename):
|
|||||||
else:
|
else:
|
||||||
return send_from_directory(app.static_folder, 'favicon.ico')
|
return send_from_directory(app.static_folder, 'favicon.ico')
|
||||||
except Exception as e:
|
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)
|
return Response("Thumbnail not found", status=404)
|
||||||
|
|
||||||
|
|
||||||
@@ -1554,7 +1566,7 @@ def preview_file(filename):
|
|||||||
else:
|
else:
|
||||||
return send_from_directory(app.static_folder, 'favicon.ico')
|
return send_from_directory(app.static_folder, 'favicon.ico')
|
||||||
except Exception as e:
|
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)
|
return Response("Preview not found", status=404)
|
||||||
|
|
||||||
|
|
||||||
@@ -2017,7 +2029,7 @@ def api_library_items():
|
|||||||
'has_more': (offset + count) < total_count
|
'has_more': (offset + count) < total_count
|
||||||
}), 200
|
}), 200
|
||||||
except Exception as e:
|
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
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
@@ -2060,6 +2072,7 @@ def api_library_scan_action():
|
|||||||
card_doc = student_cards_col.find_one({'AusweisId': student_card_id})
|
card_doc = student_cards_col.find_one({'AusweisId': student_card_id})
|
||||||
if not card_doc:
|
if not card_doc:
|
||||||
return jsonify({'ok': False, 'message': 'Ungültige Schülerausweis-ID.'}), 404
|
return jsonify({'ok': False, 'message': 'Ungültige Schülerausweis-ID.'}), 404
|
||||||
|
card_doc = _decrypt_student_card_doc(card_doc)
|
||||||
|
|
||||||
query_or = [
|
query_or = [
|
||||||
{'Code_4': item_code_raw},
|
{'Code_4': item_code_raw},
|
||||||
@@ -2227,7 +2240,7 @@ def api_item_detail(item_id):
|
|||||||
"""
|
"""
|
||||||
return detail_html, 200
|
return detail_html, 200
|
||||||
except Exception as e:
|
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
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
@@ -2330,11 +2343,6 @@ def upload_admin():
|
|||||||
try:
|
try:
|
||||||
original_item = it.get_item(duplicate_from)
|
original_item = it.get_item(duplicate_from)
|
||||||
if original_item:
|
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 = {
|
duplicate_data = {
|
||||||
'name': original_item.get('Name', ''),
|
'name': original_item.get('Name', ''),
|
||||||
'description': original_item.get('Beschreibung', ''),
|
'description': original_item.get('Beschreibung', ''),
|
||||||
@@ -2360,7 +2368,7 @@ def upload_admin():
|
|||||||
else:
|
else:
|
||||||
flash('Ursprungs-Element für Duplizierung nicht gefunden.', 'error')
|
flash('Ursprungs-Element für Duplizierung nicht gefunden.', 'error')
|
||||||
except Exception as e:
|
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')
|
flash('Fehler beim Laden der Duplizierungsdaten.', 'error')
|
||||||
|
|
||||||
# Handle the new method (sessionStorage-based duplication)
|
# Handle the new method (sessionStorage-based duplication)
|
||||||
@@ -2429,7 +2437,7 @@ def library_admin():
|
|||||||
else:
|
else:
|
||||||
flash('Ursprungs-Element für Duplizierung nicht gefunden.', 'error')
|
flash('Ursprungs-Element für Duplizierung nicht gefunden.', 'error')
|
||||||
except Exception as e:
|
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')
|
flash('Fehler beim Laden der Duplizierungsdaten.', 'error')
|
||||||
elif duplicate_flag == 'true':
|
elif duplicate_flag == 'true':
|
||||||
flash('Buch wird dupliziert. Die Daten werden aus dem Session-Speicher geladen.', 'info')
|
flash('Buch wird dupliziert. Die Daten werden aus dem Session-Speicher geladen.', 'info')
|
||||||
@@ -2476,6 +2484,7 @@ def student_cards_admin():
|
|||||||
try:
|
try:
|
||||||
card = student_cards.find_one({'_id': ObjectId(edit_id)})
|
card = student_cards.find_one({'_id': ObjectId(edit_id)})
|
||||||
if card:
|
if card:
|
||||||
|
card = _decrypt_student_card_doc(card)
|
||||||
edit_mode = True
|
edit_mode = True
|
||||||
form_data = {
|
form_data = {
|
||||||
'card_id': str(card['_id']),
|
'card_id': str(card['_id']),
|
||||||
@@ -2486,7 +2495,7 @@ def student_cards_admin():
|
|||||||
'notes': card.get('Notizen', '')
|
'notes': card.get('Notizen', '')
|
||||||
}
|
}
|
||||||
except Exception as e:
|
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')
|
flash('Fehler beim Laden des Ausweises.', 'error')
|
||||||
|
|
||||||
# Handle POST request (add or edit)
|
# Handle POST request (add or edit)
|
||||||
@@ -2504,7 +2513,7 @@ def student_cards_admin():
|
|||||||
student_cards.delete_one({'_id': ObjectId(card_id)})
|
student_cards.delete_one({'_id': ObjectId(card_id)})
|
||||||
flash('Ausweis wurde gelöscht.', 'success')
|
flash('Ausweis wurde gelöscht.', 'success')
|
||||||
except Exception as e:
|
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')
|
flash('Fehler beim Löschen des Ausweises.', 'error')
|
||||||
|
|
||||||
elif action == 'edit':
|
elif action == 'edit':
|
||||||
@@ -2518,21 +2527,27 @@ def student_cards_admin():
|
|||||||
if existing:
|
if existing:
|
||||||
flash('Diese Ausweis-ID existiert bereits.', 'error')
|
flash('Diese Ausweis-ID existiert bereits.', 'error')
|
||||||
else:
|
else:
|
||||||
|
encrypted_payload = encrypt_document_fields(
|
||||||
|
{
|
||||||
|
'SchülerName': student_name,
|
||||||
|
'Klasse': class_name,
|
||||||
|
'Notizen': notes,
|
||||||
|
},
|
||||||
|
STUDENT_CARD_ENCRYPTED_FIELDS
|
||||||
|
)
|
||||||
student_cards.update_one(
|
student_cards.update_one(
|
||||||
{'_id': ObjectId(card_id)},
|
{'_id': ObjectId(card_id)},
|
||||||
{'$set': {
|
{'$set': {
|
||||||
'AusweisId': ausweis_id,
|
'AusweisId': ausweis_id,
|
||||||
'SchülerName': student_name,
|
|
||||||
'StandardAusleihdauer': int(default_borrow_days),
|
'StandardAusleihdauer': int(default_borrow_days),
|
||||||
'Klasse': class_name,
|
'Aktualisiert': datetime.datetime.now(),
|
||||||
'Notizen': notes,
|
**encrypted_payload
|
||||||
'Aktualisiert': datetime.datetime.now()
|
|
||||||
}}
|
}}
|
||||||
)
|
)
|
||||||
flash('Ausweis wurde aktualisiert.', 'success')
|
flash('Ausweis wurde aktualisiert.', 'success')
|
||||||
return redirect(url_for('student_cards_admin'))
|
return redirect(url_for('student_cards_admin'))
|
||||||
except Exception as e:
|
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')
|
flash('Fehler beim Aktualisieren des Ausweises.', 'error')
|
||||||
|
|
||||||
elif action == 'add':
|
elif action == 'add':
|
||||||
@@ -2545,22 +2560,29 @@ def student_cards_admin():
|
|||||||
flash('Diese Ausweis-ID existiert bereits.', 'error')
|
flash('Diese Ausweis-ID existiert bereits.', 'error')
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
|
encrypted_payload = encrypt_document_fields(
|
||||||
|
{
|
||||||
|
'SchülerName': student_name,
|
||||||
|
'Klasse': class_name,
|
||||||
|
'Notizen': notes,
|
||||||
|
},
|
||||||
|
STUDENT_CARD_ENCRYPTED_FIELDS
|
||||||
|
)
|
||||||
student_cards.insert_one({
|
student_cards.insert_one({
|
||||||
'AusweisId': ausweis_id,
|
'AusweisId': ausweis_id,
|
||||||
'SchülerName': student_name,
|
|
||||||
'StandardAusleihdauer': int(default_borrow_days),
|
'StandardAusleihdauer': int(default_borrow_days),
|
||||||
'Klasse': class_name,
|
'Erstellt': datetime.datetime.now(),
|
||||||
'Notizen': notes,
|
**encrypted_payload,
|
||||||
'Erstellt': datetime.datetime.now()
|
|
||||||
})
|
})
|
||||||
flash('Neuer Ausweis wurde hinzugefügt.', 'success')
|
flash('Neuer Ausweis wurde hinzugefügt.', 'success')
|
||||||
return redirect(url_for('student_cards_admin'))
|
return redirect(url_for('student_cards_admin'))
|
||||||
except Exception as e:
|
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')
|
flash('Fehler beim Hinzufügen des Ausweises.', 'error')
|
||||||
|
|
||||||
# Get all student cards
|
# Get all student cards
|
||||||
all_cards = list(student_cards.find().sort('AusweisId', 1))
|
all_cards = list(student_cards.find().sort('AusweisId', 1))
|
||||||
|
all_cards = [_decrypt_student_card_doc(card) for card in all_cards]
|
||||||
client.close()
|
client.close()
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
@@ -2597,6 +2619,7 @@ def student_cards_print():
|
|||||||
|
|
||||||
# Get all student cards sorted by ID
|
# Get all student cards sorted by ID
|
||||||
all_cards = list(student_cards.find().sort('AusweisId', 1))
|
all_cards = list(student_cards.find().sort('AusweisId', 1))
|
||||||
|
all_cards = [_decrypt_student_card_doc(card) for card in all_cards]
|
||||||
client.close()
|
client.close()
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
@@ -2628,6 +2651,7 @@ def student_card_barcode_print():
|
|||||||
|
|
||||||
# Get all student cards sorted by ID
|
# Get all student cards sorted by ID
|
||||||
all_cards = list(student_cards.find().sort('AusweisId', 1))
|
all_cards = list(student_cards.find().sort('AusweisId', 1))
|
||||||
|
all_cards = [_decrypt_student_card_doc(card) for card in all_cards]
|
||||||
client.close()
|
client.close()
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
@@ -2667,6 +2691,7 @@ def student_card_barcode_download():
|
|||||||
db = client[cfg.MONGODB_DB]
|
db = client[cfg.MONGODB_DB]
|
||||||
student_cards = db['student_cards']
|
student_cards = db['student_cards']
|
||||||
all_cards = list(student_cards.find().sort('AusweisId', 1))
|
all_cards = list(student_cards.find().sort('AusweisId', 1))
|
||||||
|
all_cards = [_decrypt_student_card_doc(card) for card in all_cards]
|
||||||
client.close()
|
client.close()
|
||||||
|
|
||||||
pdf_buffer = BytesIO()
|
pdf_buffer = BytesIO()
|
||||||
@@ -2841,6 +2866,7 @@ def student_card_single_barcode_download(card_id):
|
|||||||
db = client[cfg.MONGODB_DB]
|
db = client[cfg.MONGODB_DB]
|
||||||
student_cards = db['student_cards']
|
student_cards = db['student_cards']
|
||||||
card = student_cards.find_one({'_id': ObjectId(card_id)})
|
card = student_cards.find_one({'_id': ObjectId(card_id)})
|
||||||
|
card = _decrypt_student_card_doc(card)
|
||||||
client.close()
|
client.close()
|
||||||
|
|
||||||
if not card:
|
if not card:
|
||||||
@@ -4516,8 +4542,28 @@ def _soft_delete_item_groups(db, root_item_ids, username):
|
|||||||
'soft_deleted_items': 0,
|
'soft_deleted_items': 0,
|
||||||
'soft_deleted_borrows': 0,
|
'soft_deleted_borrows': 0,
|
||||||
'group_item_ids': [],
|
'group_item_ids': [],
|
||||||
|
'archive': {
|
||||||
|
'archive_created': False,
|
||||||
|
'archived_files': 0,
|
||||||
|
'deleted_files': 0,
|
||||||
|
'archive_path': None,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
object_ids = []
|
||||||
|
for group_item_id in unique_group_item_ids:
|
||||||
|
try:
|
||||||
|
object_ids.append(ObjectId(group_item_id))
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
item_docs_for_archive = []
|
||||||
|
if object_ids:
|
||||||
|
item_docs_for_archive = list(db['items'].find(
|
||||||
|
{'_id': {'$in': object_ids}},
|
||||||
|
{'_id': 1, 'Images': 1}
|
||||||
|
))
|
||||||
|
|
||||||
delete_success = True
|
delete_success = True
|
||||||
soft_deleted_items = 0
|
soft_deleted_items = 0
|
||||||
|
|
||||||
@@ -4550,6 +4596,17 @@ def _soft_delete_item_groups(db, root_item_ids, username):
|
|||||||
}}
|
}}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
archive_result = {
|
||||||
|
'archive_created': False,
|
||||||
|
'archived_files': 0,
|
||||||
|
'deleted_files': 0,
|
||||||
|
'archive_path': None,
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
archive_result = encrypt_soft_deleted_media_pack(item_docs_for_archive, actor=username)
|
||||||
|
except Exception as archive_err:
|
||||||
|
app.logger.warning(f"Soft-delete media archive failed: {archive_err}")
|
||||||
|
|
||||||
_append_audit_event(
|
_append_audit_event(
|
||||||
db,
|
db,
|
||||||
'inventory_item_soft_deleted',
|
'inventory_item_soft_deleted',
|
||||||
@@ -4558,6 +4615,7 @@ def _soft_delete_item_groups(db, root_item_ids, username):
|
|||||||
'group_item_ids': unique_group_item_ids,
|
'group_item_ids': unique_group_item_ids,
|
||||||
'soft_deleted_items': soft_deleted_items,
|
'soft_deleted_items': soft_deleted_items,
|
||||||
'soft_deleted_borrow_records': borrow_result.modified_count,
|
'soft_deleted_borrow_records': borrow_result.modified_count,
|
||||||
|
'soft_delete_archive': archive_result,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -4566,6 +4624,7 @@ def _soft_delete_item_groups(db, root_item_ids, username):
|
|||||||
'soft_deleted_items': soft_deleted_items,
|
'soft_deleted_items': soft_deleted_items,
|
||||||
'soft_deleted_borrows': borrow_result.modified_count,
|
'soft_deleted_borrows': borrow_result.modified_count,
|
||||||
'group_item_ids': unique_group_item_ids,
|
'group_item_ids': unique_group_item_ids,
|
||||||
|
'archive': archive_result,
|
||||||
'message': 'OK' if delete_success else 'Fehler beim revisionssicheren Deaktivieren des Elements.',
|
'message': 'OK' if delete_success else 'Fehler beim revisionssicheren Deaktivieren des Elements.',
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4612,15 +4671,20 @@ def delete_item(id):
|
|||||||
delete_success = bool(delete_result.get('success'))
|
delete_success = bool(delete_result.get('success'))
|
||||||
soft_deleted_items = int(delete_result.get('soft_deleted_items', 0))
|
soft_deleted_items = int(delete_result.get('soft_deleted_items', 0))
|
||||||
soft_deleted_borrows = int(delete_result.get('soft_deleted_borrows', 0))
|
soft_deleted_borrows = int(delete_result.get('soft_deleted_borrows', 0))
|
||||||
|
archived_files = int((delete_result.get('archive') or {}).get('archived_files', 0))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
app.logger.error(f"Error during soft-delete for item group {id}: {str(e)}")
|
app.logger.error(f"Error during soft-delete for item group {id}: {str(e)}")
|
||||||
delete_success = False
|
delete_success = False
|
||||||
|
archived_files = 0
|
||||||
finally:
|
finally:
|
||||||
if client:
|
if client:
|
||||||
client.close()
|
client.close()
|
||||||
|
|
||||||
if delete_success:
|
if delete_success:
|
||||||
flash(f'Elementgruppe revisionssicher deaktiviert ({soft_deleted_items}/{len(group_item_ids)} Versionen).', 'success')
|
flash(
|
||||||
|
f'Elementgruppe revisionssicher deaktiviert ({soft_deleted_items}/{len(group_item_ids)} Versionen, {archived_files} Mediendateien verschlüsselt archiviert).',
|
||||||
|
'success'
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
flash('Fehler beim revisionssicheren Deaktivieren des Elements.', 'error')
|
flash('Fehler beim revisionssicheren Deaktivieren des Elements.', 'error')
|
||||||
|
|
||||||
@@ -4663,6 +4727,8 @@ def bulk_delete_items():
|
|||||||
'message': f"{result.get('soft_deleted_items', 0)} Elemente revisionssicher deaktiviert.",
|
'message': f"{result.get('soft_deleted_items', 0)} Elemente revisionssicher deaktiviert.",
|
||||||
'deleted_items': result.get('soft_deleted_items', 0),
|
'deleted_items': result.get('soft_deleted_items', 0),
|
||||||
'deleted_borrows': result.get('soft_deleted_borrows', 0),
|
'deleted_borrows': result.get('soft_deleted_borrows', 0),
|
||||||
|
'archived_files': (result.get('archive') or {}).get('archived_files', 0),
|
||||||
|
'archive_created': (result.get('archive') or {}).get('archive_created', False),
|
||||||
'group_item_ids': result.get('group_item_ids', []),
|
'group_item_ids': result.get('group_item_ids', []),
|
||||||
})
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -5196,7 +5262,7 @@ def ausleihen(id):
|
|||||||
flash('Alle Exemplare sind aufgrund geplanter Reservierungen heute belegt.', 'error')
|
flash('Alle Exemplare sind aufgrund geplanter Reservierungen heute belegt.', 'error')
|
||||||
return redirect(url_for(redirect_target))
|
return redirect(url_for(redirect_target))
|
||||||
except Exception as e:
|
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)
|
# Get number of exemplars to borrow (default to 1)
|
||||||
exemplare_count = request.form.get('exemplare_count', 1)
|
exemplare_count = request.form.get('exemplare_count', 1)
|
||||||
@@ -5317,10 +5383,7 @@ def zurueckgeben(id):
|
|||||||
|
|
||||||
username = session['username']
|
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):
|
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:
|
try:
|
||||||
# Get ALL active borrowing records for this item and complete them
|
# Get ALL active borrowing records for this item and complete them
|
||||||
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
|
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
|
||||||
@@ -5339,8 +5402,6 @@ def zurueckgeben(id):
|
|||||||
updated_count = 0
|
updated_count = 0
|
||||||
for record in active_records:
|
for record in active_records:
|
||||||
ausleihung_id = str(record['_id'])
|
ausleihung_id = str(record['_id'])
|
||||||
print(f"Completing active ausleihung {ausleihung_id} for item {id}")
|
|
||||||
|
|
||||||
# Update each active record
|
# Update each active record
|
||||||
result = ausleihungen.update_one(
|
result = ausleihungen.update_one(
|
||||||
{'_id': ObjectId(ausleihung_id)},
|
{'_id': ObjectId(ausleihung_id)},
|
||||||
|
|||||||
@@ -0,0 +1,176 @@
|
|||||||
|
"""Helpers for targeted PII encryption and encrypted archival of deleted media files."""
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import uuid
|
||||||
|
import zipfile
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from cryptography.fernet import Fernet, InvalidToken
|
||||||
|
|
||||||
|
import settings as cfg
|
||||||
|
|
||||||
|
_ENC_PREFIX = "enc::"
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_fernet_key():
|
||||||
|
"""Resolve the Fernet key from env/config or derive a stable fallback."""
|
||||||
|
configured_key = cfg.DATA_ENCRYPTION_KEY
|
||||||
|
if configured_key:
|
||||||
|
try:
|
||||||
|
# Validate the supplied key format.
|
||||||
|
Fernet(configured_key.encode("utf-8"))
|
||||||
|
return configured_key.encode("utf-8")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Fallback for compatibility: derive stable key from SECRET_KEY.
|
||||||
|
digest = hashlib.sha256(cfg.SECRET_KEY.encode("utf-8")).digest()
|
||||||
|
return base64.urlsafe_b64encode(digest)
|
||||||
|
|
||||||
|
|
||||||
|
def _fernet():
|
||||||
|
return Fernet(_resolve_fernet_key())
|
||||||
|
|
||||||
|
|
||||||
|
def encrypt_text(value):
|
||||||
|
"""Encrypt a text value. Keeps empty values unchanged."""
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
text = str(value)
|
||||||
|
if text == "" or text.startswith(_ENC_PREFIX):
|
||||||
|
return text
|
||||||
|
token = _fernet().encrypt(text.encode("utf-8")).decode("utf-8")
|
||||||
|
return f"{_ENC_PREFIX}{token}"
|
||||||
|
|
||||||
|
|
||||||
|
def decrypt_text(value):
|
||||||
|
"""Decrypt an encrypted text value. Returns original value if not encrypted."""
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
text = str(value)
|
||||||
|
if not text.startswith(_ENC_PREFIX):
|
||||||
|
return text
|
||||||
|
|
||||||
|
token = text[len(_ENC_PREFIX):]
|
||||||
|
try:
|
||||||
|
return _fernet().decrypt(token.encode("utf-8")).decode("utf-8")
|
||||||
|
except (InvalidToken, ValueError, TypeError):
|
||||||
|
# Keep data readable even if key rotation or malformed data occurred.
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def encrypt_document_fields(document, fields):
|
||||||
|
"""Encrypt selected fields of a document in-place and return it."""
|
||||||
|
for field in fields:
|
||||||
|
if field in document:
|
||||||
|
document[field] = encrypt_text(document.get(field))
|
||||||
|
return document
|
||||||
|
|
||||||
|
|
||||||
|
def decrypt_document_fields(document, fields):
|
||||||
|
"""Decrypt selected fields of a document in-place and return it."""
|
||||||
|
for field in fields:
|
||||||
|
if field in document:
|
||||||
|
document[field] = decrypt_text(document.get(field))
|
||||||
|
return document
|
||||||
|
|
||||||
|
|
||||||
|
def _candidate_media_paths(filename):
|
||||||
|
"""Return all possible filesystem paths for a stored media filename."""
|
||||||
|
name_part, _ = os.path.splitext(filename)
|
||||||
|
return [
|
||||||
|
(os.path.join(cfg.UPLOAD_FOLDER, filename), "originals"),
|
||||||
|
(os.path.join(cfg.UPLOAD_FOLDER, f"{name_part}.webp"), "originals"),
|
||||||
|
(os.path.join(cfg.UPLOAD_FOLDER, f"{name_part}.jpg"), "originals"),
|
||||||
|
(os.path.join(cfg.THUMBNAIL_FOLDER, f"{name_part}_thumb.webp"), "thumbnails"),
|
||||||
|
(os.path.join(cfg.THUMBNAIL_FOLDER, f"{name_part}_thumb.jpg"), "thumbnails"),
|
||||||
|
(os.path.join(cfg.PREVIEW_FOLDER, f"{name_part}_preview.webp"), "previews"),
|
||||||
|
(os.path.join(cfg.PREVIEW_FOLDER, f"{name_part}_preview.jpg"), "previews"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def encrypt_soft_deleted_media_pack(item_docs, *, actor="system"):
|
||||||
|
"""
|
||||||
|
Archive media files referenced by item docs, encrypt the archive, and delete originals.
|
||||||
|
|
||||||
|
Uses ZIP_STORED (no compression) to keep CPU usage low.
|
||||||
|
"""
|
||||||
|
files_to_archive = []
|
||||||
|
seen_paths = set()
|
||||||
|
|
||||||
|
for item in item_docs:
|
||||||
|
item_id = str(item.get("_id", "unknown"))
|
||||||
|
for image_name in item.get("Images", []) or []:
|
||||||
|
for abs_path, bucket in _candidate_media_paths(str(image_name)):
|
||||||
|
if abs_path in seen_paths:
|
||||||
|
continue
|
||||||
|
if not os.path.isfile(abs_path):
|
||||||
|
continue
|
||||||
|
seen_paths.add(abs_path)
|
||||||
|
files_to_archive.append((item_id, str(image_name), abs_path, bucket))
|
||||||
|
|
||||||
|
if not files_to_archive:
|
||||||
|
return {
|
||||||
|
"archive_created": False,
|
||||||
|
"archived_files": 0,
|
||||||
|
"deleted_files": 0,
|
||||||
|
"archive_path": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
os.makedirs(cfg.DELETED_ARCHIVE_FOLDER, exist_ok=True)
|
||||||
|
timestamp = datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")
|
||||||
|
archive_id = f"softdelete-{timestamp}-{uuid.uuid4().hex[:8]}"
|
||||||
|
zip_path = os.path.join(cfg.DELETED_ARCHIVE_FOLDER, f"{archive_id}.zip")
|
||||||
|
encrypted_path = os.path.join(cfg.DELETED_ARCHIVE_FOLDER, f"{archive_id}.zip.enc")
|
||||||
|
|
||||||
|
manifest = {
|
||||||
|
"archive_id": archive_id,
|
||||||
|
"created_at": datetime.utcnow().isoformat() + "Z",
|
||||||
|
"actor": actor,
|
||||||
|
"files": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
with zipfile.ZipFile(zip_path, mode="w", compression=zipfile.ZIP_STORED) as zf:
|
||||||
|
for idx, (item_id, original_name, abs_path, bucket) in enumerate(files_to_archive, start=1):
|
||||||
|
safe_name = os.path.basename(abs_path)
|
||||||
|
arcname = f"{bucket}/{item_id}/{idx:04d}-{safe_name}"
|
||||||
|
zf.write(abs_path, arcname)
|
||||||
|
manifest["files"].append(
|
||||||
|
{
|
||||||
|
"item_id": item_id,
|
||||||
|
"source_name": original_name,
|
||||||
|
"stored_as": arcname,
|
||||||
|
"size_bytes": os.path.getsize(abs_path),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
zf.writestr("manifest.json", json.dumps(manifest, ensure_ascii=False, indent=2))
|
||||||
|
|
||||||
|
with open(zip_path, "rb") as source_file:
|
||||||
|
encrypted_payload = _fernet().encrypt(source_file.read())
|
||||||
|
|
||||||
|
with open(encrypted_path, "wb") as encrypted_file:
|
||||||
|
encrypted_file.write(encrypted_payload)
|
||||||
|
|
||||||
|
deleted_files = 0
|
||||||
|
for _, _, abs_path, _ in files_to_archive:
|
||||||
|
try:
|
||||||
|
os.remove(abs_path)
|
||||||
|
deleted_files += 1
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.remove(zip_path)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return {
|
||||||
|
"archive_created": True,
|
||||||
|
"archived_files": len(files_to_archive),
|
||||||
|
"deleted_files": deleted_files,
|
||||||
|
"archive_path": encrypted_path,
|
||||||
|
}
|
||||||
@@ -11,4 +11,5 @@ pytz
|
|||||||
requests
|
requests
|
||||||
reportlab
|
reportlab
|
||||||
python-barcode
|
python-barcode
|
||||||
openpyxl
|
openpyxl
|
||||||
|
cryptography
|
||||||
+108
-12
@@ -12,6 +12,8 @@ defaults for the web application and helper modules.
|
|||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
|
import atexit
|
||||||
|
from threading import Lock
|
||||||
from pymongo import MongoClient as _PyMongoClient
|
from pymongo import MongoClient as _PyMongoClient
|
||||||
|
|
||||||
# Base directory of this Web package
|
# Base directory of this Web package
|
||||||
@@ -56,6 +58,7 @@ DEFAULTS = {
|
|||||||
'paths': {
|
'paths': {
|
||||||
'backups': os.path.join(os.path.dirname(os.path.dirname(BASE_DIR)), 'backups'),
|
'backups': os.path.join(os.path.dirname(os.path.dirname(BASE_DIR)), 'backups'),
|
||||||
'logs': os.path.join(os.path.dirname(os.path.dirname(BASE_DIR)), 'logs'),
|
'logs': os.path.join(os.path.dirname(os.path.dirname(BASE_DIR)), 'logs'),
|
||||||
|
'deleted_archives': os.path.join(os.path.dirname(os.path.dirname(BASE_DIR)), 'deleted_archives'),
|
||||||
},
|
},
|
||||||
'schoolPeriods': {
|
'schoolPeriods': {
|
||||||
"1": {"start": "08:00", "end": "08:45", "label": "1. Stunde (08:00 - 08:45)"},
|
"1": {"start": "08:00", "end": "08:45", "label": "1. Stunde (08:00 - 08:45)"},
|
||||||
@@ -99,10 +102,27 @@ def _get(conf, path, default):
|
|||||||
return default
|
return default
|
||||||
return cur
|
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
|
# Expose settings
|
||||||
APP_VERSION = _get(_conf, ['ver'], DEFAULTS['version'])
|
APP_VERSION = _get(_conf, ['ver'], DEFAULTS['version'])
|
||||||
DEBUG = _get(_conf, ['dbg'], DEFAULTS['debug'])
|
DEBUG = _get_bool_env('INVENTAR_DEBUG', _get(_conf, ['dbg'], DEFAULTS['debug']))
|
||||||
SECRET_KEY = str(_get(_conf, ['key'], DEFAULTS['secret_key']))
|
SECRET_KEY = str(os.getenv('INVENTAR_SECRET_KEY', _get(_conf, ['key'], DEFAULTS['secret_key'])))
|
||||||
HOST = _get(_conf, ['host'], DEFAULTS['host'])
|
HOST = _get(_conf, ['host'], DEFAULTS['host'])
|
||||||
PORT = _get(_conf, ['port'], DEFAULTS['port'])
|
PORT = _get(_conf, ['port'], DEFAULTS['port'])
|
||||||
|
|
||||||
@@ -115,6 +135,13 @@ MONGODB_DB = _get(_conf, ['mongodb', 'db'], DEFAULTS['mongodb']['db'])
|
|||||||
MONGODB_HOST = os.getenv('INVENTAR_MONGODB_HOST', MONGODB_HOST)
|
MONGODB_HOST = os.getenv('INVENTAR_MONGODB_HOST', MONGODB_HOST)
|
||||||
MONGODB_PORT = int(os.getenv('INVENTAR_MONGODB_PORT', str(MONGODB_PORT)))
|
MONGODB_PORT = int(os.getenv('INVENTAR_MONGODB_PORT', str(MONGODB_PORT)))
|
||||||
MONGODB_DB = os.getenv('INVENTAR_MONGODB_DB', MONGODB_DB)
|
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
|
||||||
SCHEDULER_INTERVAL_MIN = _get(_conf, ['scheduler', 'interval_minutes'], DEFAULTS['scheduler']['interval_minutes'])
|
SCHEDULER_INTERVAL_MIN = _get(_conf, ['scheduler', 'interval_minutes'], DEFAULTS['scheduler']['interval_minutes'])
|
||||||
@@ -162,10 +189,12 @@ PREVIEW_SIZE = (int(PREVIEW_SIZE_LIST[0]), int(PREVIEW_SIZE_LIST[1])) if isinsta
|
|||||||
|
|
||||||
BACKUP_FOLDER = _get(_conf, ['paths', 'backups'], DEFAULTS['paths']['backups'])
|
BACKUP_FOLDER = _get(_conf, ['paths', 'backups'], DEFAULTS['paths']['backups'])
|
||||||
LOGS_FOLDER = _get(_conf, ['paths', 'logs'], DEFAULTS['paths']['logs'])
|
LOGS_FOLDER = _get(_conf, ['paths', 'logs'], DEFAULTS['paths']['logs'])
|
||||||
|
DELETED_ARCHIVE_FOLDER = _get(_conf, ['paths', 'deleted_archives'], DEFAULTS['paths']['deleted_archives'])
|
||||||
|
|
||||||
# Optional environment overrides for writable storage mounts.
|
# Optional environment overrides for writable storage mounts.
|
||||||
BACKUP_FOLDER = os.getenv('INVENTAR_BACKUP_FOLDER', BACKUP_FOLDER)
|
BACKUP_FOLDER = os.getenv('INVENTAR_BACKUP_FOLDER', BACKUP_FOLDER)
|
||||||
LOGS_FOLDER = os.getenv('INVENTAR_LOGS_FOLDER', LOGS_FOLDER)
|
LOGS_FOLDER = os.getenv('INVENTAR_LOGS_FOLDER', LOGS_FOLDER)
|
||||||
|
DELETED_ARCHIVE_FOLDER = os.getenv('INVENTAR_DELETED_ARCHIVE_FOLDER', DELETED_ARCHIVE_FOLDER)
|
||||||
|
|
||||||
# Normalize backup and logs paths to absolute paths (similar to upload folders) to avoid
|
# Normalize backup and logs paths to absolute paths (similar to upload folders) to avoid
|
||||||
# permission issues caused by relative paths resolving to unintended working dirs.
|
# permission issues caused by relative paths resolving to unintended working dirs.
|
||||||
@@ -174,21 +203,88 @@ if not os.path.isabs(BACKUP_FOLDER):
|
|||||||
BACKUP_FOLDER = os.path.join(PROJECT_ROOT, BACKUP_FOLDER)
|
BACKUP_FOLDER = os.path.join(PROJECT_ROOT, BACKUP_FOLDER)
|
||||||
if not os.path.isabs(LOGS_FOLDER):
|
if not os.path.isabs(LOGS_FOLDER):
|
||||||
LOGS_FOLDER = os.path.join(PROJECT_ROOT, LOGS_FOLDER)
|
LOGS_FOLDER = os.path.join(PROJECT_ROOT, LOGS_FOLDER)
|
||||||
|
if not os.path.isabs(DELETED_ARCHIVE_FOLDER):
|
||||||
|
DELETED_ARCHIVE_FOLDER = os.path.join(PROJECT_ROOT, DELETED_ARCHIVE_FOLDER)
|
||||||
|
|
||||||
|
# Optional key for field/file encryption at application level.
|
||||||
|
DATA_ENCRYPTION_KEY = os.getenv('INVENTAR_DATA_ENCRYPTION_KEY', '').strip()
|
||||||
|
|
||||||
|
|
||||||
|
_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):
|
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 = {
|
client_kwargs = {
|
||||||
'maxPoolSize': 10,
|
'maxPoolSize': MONGODB_MAX_POOL_SIZE,
|
||||||
'minPoolSize': 0,
|
'minPoolSize': MONGODB_MIN_POOL_SIZE,
|
||||||
'connectTimeoutMS': 5000,
|
'maxIdleTimeMS': MONGODB_MAX_IDLE_TIME_MS,
|
||||||
'serverSelectionTimeoutMS': 5000,
|
'connectTimeoutMS': MONGODB_CONNECT_TIMEOUT_MS,
|
||||||
|
'serverSelectionTimeoutMS': MONGODB_SERVER_SELECTION_TIMEOUT_MS,
|
||||||
|
'socketTimeoutMS': MONGODB_SOCKET_TIMEOUT_MS,
|
||||||
|
'maxConnecting': MONGODB_MAX_CONNECTING,
|
||||||
'retryWrites': True,
|
'retryWrites': True,
|
||||||
|
'retryReads': True,
|
||||||
}
|
}
|
||||||
client_kwargs.update(kwargs)
|
client_kwargs.update(kwargs)
|
||||||
|
|
||||||
# Preserve caller-provided positional host/port arguments.
|
if len(args) >= 2 and not explicit_host and not explicit_port:
|
||||||
# If none are passed, use configured defaults.
|
mongo_args = args
|
||||||
if args:
|
else:
|
||||||
return _PyMongoClient(*args, **client_kwargs)
|
mongo_args = (host, port)
|
||||||
return _PyMongoClient(MONGODB_HOST, MONGODB_PORT, **client_kwargs)
|
|
||||||
|
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
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
For commercial licensing inquiries: https://github.com/AIIrondev
|
For commercial licensing inquiries: https://github.com/AIIrondev
|
||||||
-->
|
-->
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en" data-module="{{ CURRENT_MODULE }}">
|
<html lang="de" data-module="{{ CURRENT_MODULE }}">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0, user-scalable=yes">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0, user-scalable=yes">
|
||||||
|
|||||||
@@ -14,18 +14,20 @@
|
|||||||
<div class="container my-4">
|
<div class="container my-4">
|
||||||
<div class="impressum-content bg-white p-4 shadow rounded">
|
<div class="impressum-content bg-white p-4 shadow rounded">
|
||||||
<h1 class="mb-4 text-center">Impressum</h1>
|
<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">
|
<div class="impressum-section mb-4">
|
||||||
<h3 class="mb-3">Kontaktinformationen</h3>
|
<h3 class="mb-3">Kontaktinformationen</h3>
|
||||||
<p><strong>Name:</strong>Invario UG</p>
|
<address class="mb-0">
|
||||||
<p><strong>Adresse:</strong> Musterstraße 123<br>12345 Musterstadt<br>Deutschland</p>
|
<p><strong>Name:</strong> Invario UG</p>
|
||||||
<p><strong>E-Mail:</strong> <a href="mailto:kontakt@example.com">info@invario.eu</a></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>
|
||||||
|
|
||||||
<div class="impressum-section mb-4">
|
<div class="impressum-section mb-4">
|
||||||
<h3 class="mb-3">Verantwortlich für den Inhalt</h3>
|
<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>
|
<p>Invario UG<br>
|
||||||
Musterstraße 123<br>
|
Musterstraße 123<br>
|
||||||
12345 Musterstadt<br>
|
12345 Musterstadt<br>
|
||||||
@@ -36,7 +38,7 @@
|
|||||||
<h3 class="mb-3">Haftungsausschluss</h3>
|
<h3 class="mb-3">Haftungsausschluss</h3>
|
||||||
|
|
||||||
<h4 class="mb-2">Haftung für Inhalte</h4>
|
<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>
|
<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>
|
<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>
|
||||||
|
|||||||
+6
-1
@@ -30,6 +30,8 @@ services:
|
|||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
container_name: inventarsystem-app
|
container_name: inventarsystem-app
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
depends_on:
|
depends_on:
|
||||||
mongodb:
|
mongodb:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
@@ -39,17 +41,19 @@ services:
|
|||||||
INVENTAR_MONGODB_DB: Inventarsystem
|
INVENTAR_MONGODB_DB: Inventarsystem
|
||||||
INVENTAR_BACKUP_FOLDER: /data/backups
|
INVENTAR_BACKUP_FOLDER: /data/backups
|
||||||
INVENTAR_LOGS_FOLDER: /data/logs
|
INVENTAR_LOGS_FOLDER: /data/logs
|
||||||
|
INVENTAR_DELETED_ARCHIVE_FOLDER: /data/deleted-archives
|
||||||
expose:
|
expose:
|
||||||
- "8000"
|
- "8000"
|
||||||
volumes:
|
volumes:
|
||||||
- ./config.json:/app/config.json:ro
|
- ./config.json:/app/config.json:ro
|
||||||
- ./Web:/app/Web
|
- ./Web:/app/Web:ro
|
||||||
- app_uploads:/app/Web/uploads
|
- app_uploads:/app/Web/uploads
|
||||||
- app_thumbnails:/app/Web/thumbnails
|
- app_thumbnails:/app/Web/thumbnails
|
||||||
- app_previews:/app/Web/previews
|
- app_previews:/app/Web/previews
|
||||||
- app_qrcodes:/app/Web/QRCodes
|
- app_qrcodes:/app/Web/QRCodes
|
||||||
- app_backups:/data/backups
|
- app_backups:/data/backups
|
||||||
- app_logs:/data/logs
|
- app_logs:/data/logs
|
||||||
|
- app_deleted_archives:/data/deleted-archives
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
mongodb_data:
|
mongodb_data:
|
||||||
@@ -59,3 +63,4 @@ volumes:
|
|||||||
app_qrcodes:
|
app_qrcodes:
|
||||||
app_backups:
|
app_backups:
|
||||||
app_logs:
|
app_logs:
|
||||||
|
app_deleted_archives:
|
||||||
|
|||||||
+2
-1
@@ -10,4 +10,5 @@ pytz
|
|||||||
requests
|
requests
|
||||||
reportlab
|
reportlab
|
||||||
python-barcode
|
python-barcode
|
||||||
openpyxl
|
openpyxl
|
||||||
|
cryptography
|
||||||
Reference in New Issue
Block a user