Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5d8213b8c2 | |||
| b94fb1cd97 | |||
| ce20f569a3 | |||
| ec7f32994c |
+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 werkzeug.utils import secure_filename
|
||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||
import user as us
|
||||
import items as it
|
||||
import ausleihung as au
|
||||
@@ -44,6 +45,7 @@ import traceback
|
||||
import re
|
||||
import io
|
||||
import html
|
||||
import logging
|
||||
# QR Code functionality deactivated
|
||||
# import qrcode
|
||||
# from qrcode.constants import ERROR_CORRECT_L
|
||||
@@ -54,6 +56,11 @@ import uuid
|
||||
from PIL import Image, ImageOps
|
||||
import mimetypes
|
||||
import subprocess
|
||||
from data_protection import (
|
||||
decrypt_document_fields,
|
||||
encrypt_document_fields,
|
||||
encrypt_soft_deleted_media_pack,
|
||||
)
|
||||
|
||||
# Set base directory and centralized settings
|
||||
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.logger.setLevel(logging.WARNING)
|
||||
app.secret_key = cfg.SECRET_KEY
|
||||
app.debug = cfg.DEBUG
|
||||
app.config['UPLOAD_FOLDER'] = cfg.UPLOAD_FOLDER
|
||||
app.config['THUMBNAIL_FOLDER'] = cfg.THUMBNAIL_FOLDER
|
||||
app.config['PREVIEW_FOLDER'] = cfg.PREVIEW_FOLDER
|
||||
app.config['ALLOWED_EXTENSIONS'] = set(cfg.ALLOWED_EXTENSIONS)
|
||||
app.config['MAX_CONTENT_LENGTH'] = max(cfg.MAX_UPLOAD_MB, cfg.IMAGE_MAX_UPLOAD_MB, cfg.VIDEO_MAX_UPLOAD_MB) * 1024 * 1024
|
||||
app.config['SESSION_COOKIE_HTTPONLY'] = True
|
||||
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
|
||||
app.config['SESSION_COOKIE_SECURE'] = cfg.SSL_ENABLED if os.getenv('INVENTAR_SESSION_COOKIE_SECURE') is None else os.getenv('INVENTAR_SESSION_COOKIE_SECURE', '').strip().lower() in ('1', 'true', 'yes', 'on')
|
||||
app.config['PREFERRED_URL_SCHEME'] = 'https' if app.config['SESSION_COOKIE_SECURE'] else 'http'
|
||||
# app.config['QR_CODE_FOLDER'] = cfg.QR_CODE_FOLDER # QR Code storage deactivated
|
||||
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_port=1)
|
||||
|
||||
|
||||
def print(*args, **kwargs):
|
||||
if not args:
|
||||
return None
|
||||
|
||||
message = " ".join(str(arg) for arg in args)
|
||||
stripped = message.lstrip()
|
||||
if stripped.startswith(('Error', 'Fehler', 'Warning', 'Warnung', '[WARN]', '[KONFLIKT]', 'Failed', 'Fehl')):
|
||||
app.logger.warning(message)
|
||||
elif stripped.startswith(('Exception', 'Traceback')):
|
||||
app.logger.error(message)
|
||||
else:
|
||||
app.logger.info(message)
|
||||
|
||||
|
||||
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_SIZE = cfg.THUMBNAIL_SIZE
|
||||
@@ -96,6 +133,16 @@ APP_VERSION = __version__
|
||||
RELEASE_STATE_FILE = os.path.join(os.path.dirname(BASE_DIR), '.release-version')
|
||||
|
||||
|
||||
@app.after_request
|
||||
def _set_security_headers(response):
|
||||
response.headers.setdefault('X-Content-Type-Options', 'nosniff')
|
||||
response.headers.setdefault('X-Frame-Options', 'SAMEORIGIN')
|
||||
response.headers.setdefault('Referrer-Policy', 'strict-origin-when-cross-origin')
|
||||
if cfg.SSL_ENABLED:
|
||||
response.headers.setdefault('Strict-Transport-Security', 'max-age=31536000; includeSubDomains')
|
||||
return response
|
||||
|
||||
|
||||
def _get_asset_version():
|
||||
"""Return a cache-busting asset version tied to deployment state."""
|
||||
env_version = os.getenv('INVENTAR_ASSET_VERSION', '').strip()
|
||||
@@ -653,14 +700,11 @@ def create_daily_backup():
|
||||
Erstellt täglich ein Backup der Ausleihungsdatenbank
|
||||
"""
|
||||
try:
|
||||
print(f"[{datetime.datetime.now()}] Erstelle Backup der Ausleihungsdatenbank...")
|
||||
result = au.create_backup_database()
|
||||
if result:
|
||||
print(f"[{datetime.datetime.now()}] Backup erfolgreich erstellt")
|
||||
else:
|
||||
print(f"[{datetime.datetime.now()}] Fehler beim Erstellen des Backups")
|
||||
if not result:
|
||||
app.logger.warning("Daily backup creation returned false")
|
||||
except Exception as e:
|
||||
print(f"[{datetime.datetime.now()}] Ausnahme beim Erstellen des Backups: {str(e)}")
|
||||
app.logger.error(f"Daily backup creation failed: {e}")
|
||||
|
||||
def update_appointment_statuses():
|
||||
"""
|
||||
@@ -670,17 +714,8 @@ def update_appointment_statuses():
|
||||
- Aktive Termine, die beendet werden sollten
|
||||
"""
|
||||
current_time = datetime.datetime.now()
|
||||
# Prepare logging early so it's available in exception paths
|
||||
log_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'logs')
|
||||
os.makedirs(log_dir, exist_ok=True)
|
||||
log_file = os.path.join(log_dir, 'scheduler.log')
|
||||
|
||||
try:
|
||||
with open(log_file, 'a', encoding='utf-8') as f:
|
||||
f.write(f"[{current_time}] Starte automatische Statusaktualisierung...\n")
|
||||
|
||||
print(f"[{current_time}] Starte automatische Statusaktualisierung...")
|
||||
|
||||
# Hole alle Termine mit Status 'planned' oder 'active'
|
||||
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
|
||||
db = client[MONGODB_DB]
|
||||
@@ -742,16 +777,15 @@ def update_appointment_statuses():
|
||||
f" [KONFLIKT] Termin {appointment['_id']}: "
|
||||
f"planned → active, aber {conflict_note}"
|
||||
)
|
||||
print(conflict_log)
|
||||
with open(log_file, 'a', encoding='utf-8') as f:
|
||||
f.write(f"[{current_time}] {conflict_log}\n")
|
||||
app.logger.warning(conflict_log)
|
||||
else:
|
||||
# No conflict — clear any previously stored conflict flag
|
||||
extra_fields['ConflictDetected'] = False
|
||||
extra_fields['ConflictNote'] = ''
|
||||
except Exception as conflict_err:
|
||||
print(f" [WARN] Konfliktprüfung fehlgeschlagen für Termin "
|
||||
f"{appointment['_id']}: {conflict_err}")
|
||||
app.logger.warning(
|
||||
f"Conflict check failed for appointment {appointment['_id']}: {conflict_err}"
|
||||
)
|
||||
|
||||
result = ausleihungen.update_one(
|
||||
{'_id': appointment['_id']},
|
||||
@@ -769,37 +803,15 @@ def update_appointment_statuses():
|
||||
elif new_status == 'completed':
|
||||
completed_count += 1
|
||||
|
||||
log_msg = f" - Termin {appointment['_id']}: {old_status} → {new_status}"
|
||||
print(log_msg)
|
||||
with open(log_file, 'a', encoding='utf-8') as f:
|
||||
f.write(f"[{current_time}] {log_msg}\n")
|
||||
|
||||
client.close()
|
||||
|
||||
if updated_count > 0:
|
||||
result_msg = f"Statusaktualisierung abgeschlossen: {updated_count} Termine aktualisiert"
|
||||
detail_msg = f" - {activated_count} aktiviert, {completed_count} abgeschlossen"
|
||||
print(f"[{current_time}] {result_msg}")
|
||||
print(f"[{current_time}] {detail_msg}")
|
||||
with open(log_file, 'a', encoding='utf-8') as f:
|
||||
f.write(f"[{current_time}] {result_msg}\n")
|
||||
f.write(f"[{current_time}] {detail_msg}\n")
|
||||
else:
|
||||
result_msg = "Statusaktualisierung abgeschlossen: Keine Änderungen erforderlich"
|
||||
print(f"[{current_time}] {result_msg}")
|
||||
with open(log_file, 'a', encoding='utf-8') as f:
|
||||
f.write(f"[{current_time}] {result_msg}\n")
|
||||
app.logger.warning(
|
||||
f"Appointment status update finished: {updated_count} changed ({activated_count} active, {completed_count} completed)"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Fehler bei der automatischen Statusaktualisierung: {str(e)}"
|
||||
print(f"[{datetime.datetime.now()}] {error_msg}")
|
||||
try:
|
||||
with open(log_file, 'a', encoding='utf-8') as f:
|
||||
f.write(f"[{datetime.datetime.now()}] {error_msg}\n")
|
||||
except Exception:
|
||||
pass
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
app.logger.error(f"Automatic appointment status update failed: {e}")
|
||||
|
||||
# Schedule jobs
|
||||
scheduler = BackgroundScheduler()
|
||||
@@ -1484,7 +1496,7 @@ def uploaded_file(filename):
|
||||
# Default placeholder from static folder
|
||||
return send_from_directory(app.static_folder, 'favicon.ico')
|
||||
except Exception as e:
|
||||
print(f"Error serving file {filename}: {str(e)}")
|
||||
app.logger.error(f"Error serving file {filename}: {str(e)}")
|
||||
return Response("Image not found", status=404)
|
||||
|
||||
|
||||
@@ -1519,7 +1531,7 @@ def thumbnail_file(filename):
|
||||
else:
|
||||
return send_from_directory(app.static_folder, 'favicon.ico')
|
||||
except Exception as e:
|
||||
print(f"Error serving thumbnail {filename}: {str(e)}")
|
||||
app.logger.error(f"Error serving thumbnail {filename}: {str(e)}")
|
||||
return Response("Thumbnail not found", status=404)
|
||||
|
||||
|
||||
@@ -1554,7 +1566,7 @@ def preview_file(filename):
|
||||
else:
|
||||
return send_from_directory(app.static_folder, 'favicon.ico')
|
||||
except Exception as e:
|
||||
print(f"Error serving preview {filename}: {str(e)}")
|
||||
app.logger.error(f"Error serving preview {filename}: {str(e)}")
|
||||
return Response("Preview not found", status=404)
|
||||
|
||||
|
||||
@@ -2017,7 +2029,7 @@ def api_library_items():
|
||||
'has_more': (offset + count) < total_count
|
||||
}), 200
|
||||
except Exception as e:
|
||||
print(f"Error fetching library items: {e}")
|
||||
app.logger.error(f"Error fetching library items: {e}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@@ -2060,6 +2072,7 @@ def api_library_scan_action():
|
||||
card_doc = student_cards_col.find_one({'AusweisId': student_card_id})
|
||||
if not card_doc:
|
||||
return jsonify({'ok': False, 'message': 'Ungültige Schülerausweis-ID.'}), 404
|
||||
card_doc = _decrypt_student_card_doc(card_doc)
|
||||
|
||||
query_or = [
|
||||
{'Code_4': item_code_raw},
|
||||
@@ -2227,7 +2240,7 @@ def api_item_detail(item_id):
|
||||
"""
|
||||
return detail_html, 200
|
||||
except Exception as e:
|
||||
print(f"Error fetching item detail: {e}")
|
||||
app.logger.error(f"Error fetching item detail: {e}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@@ -2330,11 +2343,6 @@ def upload_admin():
|
||||
try:
|
||||
original_item = it.get_item(duplicate_from)
|
||||
if original_item:
|
||||
# Enhanced debug logging for images
|
||||
images = original_item.get('Images', [])
|
||||
print(f"DEBUG: Original item: {original_item.get('_id')} has these images: {images}")
|
||||
print(f"DEBUG: Images type: {type(images)}, count: {len(images) if isinstance(images, list) else 'not a list'}")
|
||||
|
||||
duplicate_data = {
|
||||
'name': original_item.get('Name', ''),
|
||||
'description': original_item.get('Beschreibung', ''),
|
||||
@@ -2360,7 +2368,7 @@ def upload_admin():
|
||||
else:
|
||||
flash('Ursprungs-Element für Duplizierung nicht gefunden.', 'error')
|
||||
except Exception as e:
|
||||
print(f"Error loading item for duplication: {e}")
|
||||
app.logger.warning(f"Error loading item for duplication: {e}")
|
||||
flash('Fehler beim Laden der Duplizierungsdaten.', 'error')
|
||||
|
||||
# Handle the new method (sessionStorage-based duplication)
|
||||
@@ -2429,7 +2437,7 @@ def library_admin():
|
||||
else:
|
||||
flash('Ursprungs-Element für Duplizierung nicht gefunden.', 'error')
|
||||
except Exception as e:
|
||||
print(f"Error loading item for duplication: {e}")
|
||||
app.logger.warning(f"Error loading item for duplication: {e}")
|
||||
flash('Fehler beim Laden der Duplizierungsdaten.', 'error')
|
||||
elif duplicate_flag == 'true':
|
||||
flash('Buch wird dupliziert. Die Daten werden aus dem Session-Speicher geladen.', 'info')
|
||||
@@ -2476,6 +2484,7 @@ def student_cards_admin():
|
||||
try:
|
||||
card = student_cards.find_one({'_id': ObjectId(edit_id)})
|
||||
if card:
|
||||
card = _decrypt_student_card_doc(card)
|
||||
edit_mode = True
|
||||
form_data = {
|
||||
'card_id': str(card['_id']),
|
||||
@@ -2486,7 +2495,7 @@ def student_cards_admin():
|
||||
'notes': card.get('Notizen', '')
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"Error loading student card for edit: {e}")
|
||||
app.logger.error(f"Error loading student card for edit: {e}")
|
||||
flash('Fehler beim Laden des Ausweises.', 'error')
|
||||
|
||||
# Handle POST request (add or edit)
|
||||
@@ -2504,7 +2513,7 @@ def student_cards_admin():
|
||||
student_cards.delete_one({'_id': ObjectId(card_id)})
|
||||
flash('Ausweis wurde gelöscht.', 'success')
|
||||
except Exception as e:
|
||||
print(f"Error deleting student card: {e}")
|
||||
app.logger.error(f"Error deleting student card: {e}")
|
||||
flash('Fehler beim Löschen des Ausweises.', 'error')
|
||||
|
||||
elif action == 'edit':
|
||||
@@ -2518,21 +2527,27 @@ def student_cards_admin():
|
||||
if existing:
|
||||
flash('Diese Ausweis-ID existiert bereits.', 'error')
|
||||
else:
|
||||
encrypted_payload = encrypt_document_fields(
|
||||
{
|
||||
'SchülerName': student_name,
|
||||
'Klasse': class_name,
|
||||
'Notizen': notes,
|
||||
},
|
||||
STUDENT_CARD_ENCRYPTED_FIELDS
|
||||
)
|
||||
student_cards.update_one(
|
||||
{'_id': ObjectId(card_id)},
|
||||
{'$set': {
|
||||
'AusweisId': ausweis_id,
|
||||
'SchülerName': student_name,
|
||||
'StandardAusleihdauer': int(default_borrow_days),
|
||||
'Klasse': class_name,
|
||||
'Notizen': notes,
|
||||
'Aktualisiert': datetime.datetime.now()
|
||||
'Aktualisiert': datetime.datetime.now(),
|
||||
**encrypted_payload
|
||||
}}
|
||||
)
|
||||
flash('Ausweis wurde aktualisiert.', 'success')
|
||||
return redirect(url_for('student_cards_admin'))
|
||||
except Exception as e:
|
||||
print(f"Error updating student card: {e}")
|
||||
app.logger.error(f"Error updating student card: {e}")
|
||||
flash('Fehler beim Aktualisieren des Ausweises.', 'error')
|
||||
|
||||
elif action == 'add':
|
||||
@@ -2545,22 +2560,29 @@ def student_cards_admin():
|
||||
flash('Diese Ausweis-ID existiert bereits.', 'error')
|
||||
else:
|
||||
try:
|
||||
encrypted_payload = encrypt_document_fields(
|
||||
{
|
||||
'SchülerName': student_name,
|
||||
'Klasse': class_name,
|
||||
'Notizen': notes,
|
||||
},
|
||||
STUDENT_CARD_ENCRYPTED_FIELDS
|
||||
)
|
||||
student_cards.insert_one({
|
||||
'AusweisId': ausweis_id,
|
||||
'SchülerName': student_name,
|
||||
'StandardAusleihdauer': int(default_borrow_days),
|
||||
'Klasse': class_name,
|
||||
'Notizen': notes,
|
||||
'Erstellt': datetime.datetime.now()
|
||||
'Erstellt': datetime.datetime.now(),
|
||||
**encrypted_payload,
|
||||
})
|
||||
flash('Neuer Ausweis wurde hinzugefügt.', 'success')
|
||||
return redirect(url_for('student_cards_admin'))
|
||||
except Exception as e:
|
||||
print(f"Error adding student card: {e}")
|
||||
app.logger.error(f"Error adding student card: {e}")
|
||||
flash('Fehler beim Hinzufügen des Ausweises.', 'error')
|
||||
|
||||
# Get all student cards
|
||||
all_cards = list(student_cards.find().sort('AusweisId', 1))
|
||||
all_cards = [_decrypt_student_card_doc(card) for card in all_cards]
|
||||
client.close()
|
||||
|
||||
return render_template(
|
||||
@@ -2597,6 +2619,7 @@ def student_cards_print():
|
||||
|
||||
# Get all student cards sorted by ID
|
||||
all_cards = list(student_cards.find().sort('AusweisId', 1))
|
||||
all_cards = [_decrypt_student_card_doc(card) for card in all_cards]
|
||||
client.close()
|
||||
|
||||
return render_template(
|
||||
@@ -2628,6 +2651,7 @@ def student_card_barcode_print():
|
||||
|
||||
# Get all student cards sorted by ID
|
||||
all_cards = list(student_cards.find().sort('AusweisId', 1))
|
||||
all_cards = [_decrypt_student_card_doc(card) for card in all_cards]
|
||||
client.close()
|
||||
|
||||
return render_template(
|
||||
@@ -2667,6 +2691,7 @@ def student_card_barcode_download():
|
||||
db = client[cfg.MONGODB_DB]
|
||||
student_cards = db['student_cards']
|
||||
all_cards = list(student_cards.find().sort('AusweisId', 1))
|
||||
all_cards = [_decrypt_student_card_doc(card) for card in all_cards]
|
||||
client.close()
|
||||
|
||||
pdf_buffer = BytesIO()
|
||||
@@ -2841,6 +2866,7 @@ def student_card_single_barcode_download(card_id):
|
||||
db = client[cfg.MONGODB_DB]
|
||||
student_cards = db['student_cards']
|
||||
card = student_cards.find_one({'_id': ObjectId(card_id)})
|
||||
card = _decrypt_student_card_doc(card)
|
||||
client.close()
|
||||
|
||||
if not card:
|
||||
@@ -4516,8 +4542,28 @@ def _soft_delete_item_groups(db, root_item_ids, username):
|
||||
'soft_deleted_items': 0,
|
||||
'soft_deleted_borrows': 0,
|
||||
'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
|
||||
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(
|
||||
db,
|
||||
'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,
|
||||
'soft_deleted_items': soft_deleted_items,
|
||||
'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_borrows': borrow_result.modified_count,
|
||||
'group_item_ids': unique_group_item_ids,
|
||||
'archive': archive_result,
|
||||
'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'))
|
||||
soft_deleted_items = int(delete_result.get('soft_deleted_items', 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:
|
||||
app.logger.error(f"Error during soft-delete for item group {id}: {str(e)}")
|
||||
delete_success = False
|
||||
archived_files = 0
|
||||
finally:
|
||||
if client:
|
||||
client.close()
|
||||
|
||||
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:
|
||||
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.",
|
||||
'deleted_items': result.get('soft_deleted_items', 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', []),
|
||||
})
|
||||
except Exception as e:
|
||||
@@ -5196,7 +5262,7 @@ def ausleihen(id):
|
||||
flash('Alle Exemplare sind aufgrund geplanter Reservierungen heute belegt.', 'error')
|
||||
return redirect(url_for(redirect_target))
|
||||
except Exception as e:
|
||||
print(f"Warning: could not enforce planned booking guard: {e}")
|
||||
app.logger.warning(f"Could not enforce planned booking guard: {e}")
|
||||
|
||||
# Get number of exemplars to borrow (default to 1)
|
||||
exemplare_count = request.form.get('exemplare_count', 1)
|
||||
@@ -5317,10 +5383,7 @@ def zurueckgeben(id):
|
||||
|
||||
username = session['username']
|
||||
|
||||
print("Code 1169: zurueckgeben called with item ID:", id)
|
||||
|
||||
if not item.get('Verfuegbar', True) and (us.check_admin(session['username']) or item.get('User') == username):
|
||||
print("Code 1172: Item is not available, proceeding with return")
|
||||
try:
|
||||
# Get ALL active borrowing records for this item and complete them
|
||||
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
|
||||
@@ -5339,8 +5402,6 @@ def zurueckgeben(id):
|
||||
updated_count = 0
|
||||
for record in active_records:
|
||||
ausleihung_id = str(record['_id'])
|
||||
print(f"Completing active ausleihung {ausleihung_id} for item {id}")
|
||||
|
||||
# Update each active record
|
||||
result = ausleihungen.update_one(
|
||||
{'_id': ObjectId(ausleihung_id)},
|
||||
|
||||
@@ -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
|
||||
reportlab
|
||||
python-barcode
|
||||
openpyxl
|
||||
openpyxl
|
||||
cryptography
|
||||
+108
-12
@@ -12,6 +12,8 @@ defaults for the web application and helper modules.
|
||||
"""
|
||||
import os
|
||||
import json
|
||||
import atexit
|
||||
from threading import Lock
|
||||
from pymongo import MongoClient as _PyMongoClient
|
||||
|
||||
# Base directory of this Web package
|
||||
@@ -56,6 +58,7 @@ DEFAULTS = {
|
||||
'paths': {
|
||||
'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'),
|
||||
'deleted_archives': os.path.join(os.path.dirname(os.path.dirname(BASE_DIR)), 'deleted_archives'),
|
||||
},
|
||||
'schoolPeriods': {
|
||||
"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 cur
|
||||
|
||||
|
||||
def _get_bool_env(name, default):
|
||||
value = os.getenv(name)
|
||||
if value is None:
|
||||
return default
|
||||
return value.strip().lower() in ('1', 'true', 'yes', 'on')
|
||||
|
||||
|
||||
def _get_int_env(name, default):
|
||||
value = os.getenv(name)
|
||||
if value is None or not value.strip():
|
||||
return int(default)
|
||||
try:
|
||||
return int(value)
|
||||
except ValueError:
|
||||
return int(default)
|
||||
|
||||
# Expose settings
|
||||
APP_VERSION = _get(_conf, ['ver'], DEFAULTS['version'])
|
||||
DEBUG = _get(_conf, ['dbg'], DEFAULTS['debug'])
|
||||
SECRET_KEY = str(_get(_conf, ['key'], DEFAULTS['secret_key']))
|
||||
DEBUG = _get_bool_env('INVENTAR_DEBUG', _get(_conf, ['dbg'], DEFAULTS['debug']))
|
||||
SECRET_KEY = str(os.getenv('INVENTAR_SECRET_KEY', _get(_conf, ['key'], DEFAULTS['secret_key'])))
|
||||
HOST = _get(_conf, ['host'], DEFAULTS['host'])
|
||||
PORT = _get(_conf, ['port'], DEFAULTS['port'])
|
||||
|
||||
@@ -115,6 +135,13 @@ MONGODB_DB = _get(_conf, ['mongodb', 'db'], DEFAULTS['mongodb']['db'])
|
||||
MONGODB_HOST = os.getenv('INVENTAR_MONGODB_HOST', MONGODB_HOST)
|
||||
MONGODB_PORT = int(os.getenv('INVENTAR_MONGODB_PORT', str(MONGODB_PORT)))
|
||||
MONGODB_DB = os.getenv('INVENTAR_MONGODB_DB', MONGODB_DB)
|
||||
MONGODB_MAX_POOL_SIZE = _get_int_env('INVENTAR_MONGODB_MAX_POOL_SIZE', 20)
|
||||
MONGODB_MIN_POOL_SIZE = _get_int_env('INVENTAR_MONGODB_MIN_POOL_SIZE', 0)
|
||||
MONGODB_MAX_IDLE_TIME_MS = _get_int_env('INVENTAR_MONGODB_MAX_IDLE_TIME_MS', 300000)
|
||||
MONGODB_CONNECT_TIMEOUT_MS = _get_int_env('INVENTAR_MONGODB_CONNECT_TIMEOUT_MS', 5000)
|
||||
MONGODB_SERVER_SELECTION_TIMEOUT_MS = _get_int_env('INVENTAR_MONGODB_SERVER_SELECTION_TIMEOUT_MS', 5000)
|
||||
MONGODB_SOCKET_TIMEOUT_MS = _get_int_env('INVENTAR_MONGODB_SOCKET_TIMEOUT_MS', 30000)
|
||||
MONGODB_MAX_CONNECTING = _get_int_env('INVENTAR_MONGODB_MAX_CONNECTING', 2)
|
||||
|
||||
# Scheduler
|
||||
SCHEDULER_INTERVAL_MIN = _get(_conf, ['scheduler', 'interval_minutes'], DEFAULTS['scheduler']['interval_minutes'])
|
||||
@@ -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'])
|
||||
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.
|
||||
BACKUP_FOLDER = os.getenv('INVENTAR_BACKUP_FOLDER', BACKUP_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
|
||||
# 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)
|
||||
if not os.path.isabs(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):
|
||||
"""Return a lightweight MongoDB client configured from this settings module."""
|
||||
"""Return a process-local MongoDB client configured from this settings module."""
|
||||
explicit_host = 'host' in kwargs
|
||||
explicit_port = 'port' in kwargs
|
||||
host = args[0] if len(args) >= 1 else kwargs.pop('host', MONGODB_HOST)
|
||||
port = args[1] if len(args) >= 2 else kwargs.pop('port', MONGODB_PORT)
|
||||
client_kwargs = {
|
||||
'maxPoolSize': 10,
|
||||
'minPoolSize': 0,
|
||||
'connectTimeoutMS': 5000,
|
||||
'serverSelectionTimeoutMS': 5000,
|
||||
'maxPoolSize': MONGODB_MAX_POOL_SIZE,
|
||||
'minPoolSize': MONGODB_MIN_POOL_SIZE,
|
||||
'maxIdleTimeMS': MONGODB_MAX_IDLE_TIME_MS,
|
||||
'connectTimeoutMS': MONGODB_CONNECT_TIMEOUT_MS,
|
||||
'serverSelectionTimeoutMS': MONGODB_SERVER_SELECTION_TIMEOUT_MS,
|
||||
'socketTimeoutMS': MONGODB_SOCKET_TIMEOUT_MS,
|
||||
'maxConnecting': MONGODB_MAX_CONNECTING,
|
||||
'retryWrites': True,
|
||||
'retryReads': True,
|
||||
}
|
||||
client_kwargs.update(kwargs)
|
||||
|
||||
# Preserve caller-provided positional host/port arguments.
|
||||
# If none are passed, use configured defaults.
|
||||
if args:
|
||||
return _PyMongoClient(*args, **client_kwargs)
|
||||
return _PyMongoClient(MONGODB_HOST, MONGODB_PORT, **client_kwargs)
|
||||
if len(args) >= 2 and not explicit_host and not explicit_port:
|
||||
mongo_args = args
|
||||
else:
|
||||
mongo_args = (host, port)
|
||||
|
||||
cache_key = (
|
||||
mongo_args,
|
||||
tuple(sorted((key, repr(value)) for key, value in client_kwargs.items())),
|
||||
)
|
||||
|
||||
with _MONGO_CLIENT_LOCK:
|
||||
cached_client = _MONGO_CLIENT_CACHE.get(cache_key)
|
||||
if cached_client is not None:
|
||||
return cached_client
|
||||
|
||||
client = _PyMongoClient(*mongo_args, **client_kwargs)
|
||||
|
||||
cached_client = _MongoClientProxy(client)
|
||||
_MONGO_CLIENT_CACHE[cache_key] = cached_client
|
||||
return cached_client
|
||||
|
||||
+3
-13
@@ -7,7 +7,7 @@
|
||||
For commercial licensing inquiries: https://github.com/AIIrondev
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-module="{{ CURRENT_MODULE }}">
|
||||
<html lang="de" data-module="{{ CURRENT_MODULE }}">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0, user-scalable=yes">
|
||||
@@ -487,12 +487,7 @@
|
||||
<a class="nav-link quick-link-pill {% if current_path == url_for('my_borrowed_items') %}nav-active{% endif %}" href="{{ url_for('my_borrowed_items') }}">Meine Ausleihen</a>
|
||||
</li>
|
||||
<li class="nav-item" data-nav-fixed="true">
|
||||
<a class="nav-link quick-link-pill {% if current_path == url_for('notifications_view') %}nav-active{% endif %}" href="{{ url_for('notifications_view') }}">
|
||||
Benachrichtigungen
|
||||
{% if unread_notification_count and unread_notification_count > 0 %}
|
||||
<span class="notif-badge">{{ unread_notification_count }}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
<a class="nav-link quick-link-pill {% if current_path == url_for('tutorial_page') %}nav-active{% endif %}" href="{{ url_for('tutorial_page') }}">Tutorial</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if 'username' in session and (session.get('admin', False) or is_admin) %}
|
||||
@@ -583,12 +578,7 @@
|
||||
<a class="nav-link quick-link-pill {% if current_path == url_for('my_borrowed_items') %}nav-active{% endif %}" href="{{ url_for('my_borrowed_items') }}">Meine Medien</a>
|
||||
</li>
|
||||
<li class="nav-item" data-nav-fixed="true">
|
||||
<a class="nav-link quick-link-pill {% if current_path == url_for('notifications_view') %}nav-active{% endif %}" href="{{ url_for('notifications_view') }}">
|
||||
Benachrichtigungen
|
||||
{% if unread_notification_count and unread_notification_count > 0 %}
|
||||
<span class="notif-badge">{{ unread_notification_count }}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
<a class="nav-link quick-link-pill {% if current_path == url_for('tutorial_page') %}nav-active{% endif %}" href="{{ url_for('tutorial_page') }}">Tutorial</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if 'username' in session and (session.get('admin', False) or is_admin) %}
|
||||
|
||||
@@ -14,20 +14,21 @@
|
||||
<div class="container my-4">
|
||||
<div class="impressum-content bg-white p-4 shadow rounded">
|
||||
<h1 class="mb-4 text-center">Impressum</h1>
|
||||
<p class="text-center mb-5">(Angaben gemäß § 5 TMG)</p>
|
||||
<p class="text-center mb-5">(Angaben gemäß § 5 DDG)</p>
|
||||
|
||||
<div class="impressum-section mb-4">
|
||||
<h3 class="mb-3">Kontaktinformationen</h3>
|
||||
<p><strong>Name:</strong> . ..</p>
|
||||
<p><strong>Adresse:</strong> Musterstraße 123<br>12345 Musterstadt<br>Deutschland</p>
|
||||
<p><strong>E-Mail:</strong> <a href="mailto:kontakt@example.com">kontakt@example.com</a></p>
|
||||
<p><strong>Telefon:</strong> +49 123 456789</p>
|
||||
<address class="mb-0">
|
||||
<p><strong>Name:</strong> Invario UG</p>
|
||||
<p><strong>Adresse:</strong> Musterstraße 123<br>12345 Musterstadt<br>Deutschland</p>
|
||||
</address>
|
||||
<p><strong>E-Mail:</strong> <a href="mailto:info@invario.eu">info@invario.eu</a></p>
|
||||
</div>
|
||||
|
||||
<div class="impressum-section mb-4">
|
||||
<h3 class="mb-3">Verantwortlich für den Inhalt</h3>
|
||||
<p>(nach § 55 Abs. 2 RStV)</p>
|
||||
<p>...<br>
|
||||
<p>(nach § 18 Abs. 2 MStV)</p>
|
||||
<p>Invario UG<br>
|
||||
Musterstraße 123<br>
|
||||
12345 Musterstadt<br>
|
||||
Deutschland</p>
|
||||
@@ -37,7 +38,7 @@
|
||||
<h3 class="mb-3">Haftungsausschluss</h3>
|
||||
|
||||
<h4 class="mb-2">Haftung für Inhalte</h4>
|
||||
<p>Die Inhalte unserer Seiten wurden mit größter Sorgfalt erstellt. Für die Richtigkeit, Vollständigkeit und Aktualität der Inhalte können wir jedoch keine Gewähr übernehmen. Als Diensteanbieter sind wir gemäß § 7 Abs.1 TMG für eigene Inhalte auf diesen Seiten nach den allgemeinen Gesetzen verantwortlich. Nach §§ 8 bis 10 TMG sind wir als Diensteanbieter jedoch nicht verpflichtet, übermittelte oder gespeicherte fremde Informationen zu überwachen oder nach Umständen zu forschen, die auf eine rechtswidrige Tätigkeit hinweisen.</p>
|
||||
<p>Die Inhalte unserer Seiten wurden mit größter Sorgfalt erstellt. Für die Richtigkeit, Vollständigkeit und Aktualität der Inhalte können wir jedoch keine Gewähr übernehmen. Als Diensteanbieter sind wir gemäß § 7 Abs. 1 DDG für eigene Inhalte auf diesen Seiten nach den allgemeinen Gesetzen verantwortlich. Nach §§ 8 bis 10 DDG sind wir als Diensteanbieter jedoch nicht verpflichtet, übermittelte oder gespeicherte fremde Informationen zu überwachen oder nach Umständen zu forschen, die auf eine rechtswidrige Tätigkeit hinweisen.</p>
|
||||
|
||||
<h4 class="mb-2">Haftung für Links</h4>
|
||||
<p>Unser Angebot enthält Links zu externen Websites Dritter, auf deren Inhalte wir keinen Einfluss haben. Deshalb können wir für diese fremden Inhalte auch keine Gewähr übernehmen. Für die Inhalte der verlinkten Seiten ist stets der jeweilige Anbieter oder Betreiber der Seiten verantwortlich. Die verlinkten Seiten wurden zum Zeitpunkt der Verlinkung auf mögliche Rechtsverstöße überprüft. Rechtswidrige Inhalte waren zum Zeitpunkt der Verlinkung nicht erkennbar.</p>
|
||||
|
||||
@@ -321,11 +321,6 @@
|
||||
<li><strong>Einwilligung:</strong> Falls private Daten der Nutzer erfasst werden, muss eine explizite
|
||||
Einwilligung vorliegen (Art. 6 Abs. 1 lit. a DSGVO).</li>
|
||||
</ol>
|
||||
<div class="license-exception-notice" style="background-color:#f8d7da; border-color:#f5c2c7; border-left-color:#dc3545;">
|
||||
<h3 style="color:#842029;">⚠️ Hinweis</h3>
|
||||
<p>Dieses Dokument stellt <strong>keine Rechtsberatung</strong> dar. Bitte konsultieren Sie im Zweifelsfall
|
||||
einen Fachanwalt für Datenschutzrecht.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- /#pane-legal -->
|
||||
|
||||
|
||||
+6
-1
@@ -30,6 +30,8 @@ services:
|
||||
dockerfile: Dockerfile
|
||||
container_name: inventarsystem-app
|
||||
restart: unless-stopped
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
depends_on:
|
||||
mongodb:
|
||||
condition: service_healthy
|
||||
@@ -39,17 +41,19 @@ services:
|
||||
INVENTAR_MONGODB_DB: Inventarsystem
|
||||
INVENTAR_BACKUP_FOLDER: /data/backups
|
||||
INVENTAR_LOGS_FOLDER: /data/logs
|
||||
INVENTAR_DELETED_ARCHIVE_FOLDER: /data/deleted-archives
|
||||
expose:
|
||||
- "8000"
|
||||
volumes:
|
||||
- ./config.json:/app/config.json:ro
|
||||
- ./Web:/app/Web
|
||||
- ./Web:/app/Web:ro
|
||||
- app_uploads:/app/Web/uploads
|
||||
- app_thumbnails:/app/Web/thumbnails
|
||||
- app_previews:/app/Web/previews
|
||||
- app_qrcodes:/app/Web/QRCodes
|
||||
- app_backups:/data/backups
|
||||
- app_logs:/data/logs
|
||||
- app_deleted_archives:/data/deleted-archives
|
||||
|
||||
volumes:
|
||||
mongodb_data:
|
||||
@@ -59,3 +63,4 @@ volumes:
|
||||
app_qrcodes:
|
||||
app_backups:
|
||||
app_logs:
|
||||
app_deleted_archives:
|
||||
|
||||
+2
-1
@@ -10,4 +10,5 @@ pytz
|
||||
requests
|
||||
reportlab
|
||||
python-barcode
|
||||
openpyxl
|
||||
openpyxl
|
||||
cryptography
|
||||
Reference in New Issue
Block a user