Compare commits

..

3 Commits

8 changed files with 441 additions and 99 deletions
+138 -77
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 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)},
+176
View File
@@ -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,
}
+2 -1
View File
@@ -11,4 +11,5 @@ pytz
requests requests
reportlab reportlab
python-barcode python-barcode
openpyxl openpyxl
cryptography
+108 -12
View File
@@ -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
+1 -1
View File
@@ -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">
+8 -6
View File
@@ -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
View File
@@ -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
View File
@@ -10,4 +10,5 @@ pytz
requests requests
reportlab reportlab
python-barcode python-barcode
openpyxl openpyxl
cryptography