some more savety processing

This commit is contained in:
2026-06-27 13:28:49 +02:00
parent 96245bb06b
commit 7e258e07ab
3 changed files with 96 additions and 144 deletions
+24 -32
View File
@@ -79,6 +79,7 @@ from Web.modules.inventarsystem.data_protection import (
decrypt_text,
encrypt_document_fields,
encrypt_soft_deleted_media_pack,
encrypt_text,
)
from Web.modules.terminplaner.blueprint import appoint_bp as terminplaner_bp
@@ -669,7 +670,6 @@ def _get_current_module(path):
last_module = session.get('last_module')
if last_module and cfg.MODULES.is_enabled(last_module):
return last_module
if cfg.MODULES.is_enabled('inventory'):
return 'inventory'
return 'library' if cfg.MODULES.is_enabled('library') else 'inventory'
@@ -726,9 +726,9 @@ def _append_audit_event_standalone(event_type, payload):
al.append_audit_event(
db=db,
event_type=event_type,
actor=session.get('username', 'system'),
payload=payload,
request_ip=request.remote_addr,
actor=encrypt_text(session.get('username', 'system')),
payload=encrypt_text(str(payload)),
request_ip=encrypt_text(request.remote_addr),
source='web',
)
except Exception as exc:
@@ -950,7 +950,7 @@ def _create_notification(db, *, audience, notif_type, title, message, target_use
tag=f'notification-{notif_type}'
)
except Exception as e:
app.logger.warning(f'Failed to send push notification to {target_user}: {e}')
app.logger.warning(f'Failed to send push notification to {encrypt_text(target_user)}: {e}')
elif audience == 'admin':
_bump_notification_version('admin')
# Send push notification to all admins
@@ -1053,7 +1053,7 @@ def _get_cached_unread_status(username, is_admin=False):
if cached:
return json.loads(cached), version_tag
except Exception as exc:
app.logger.warning(f'Could not read notification status cache for {username}: {exc}')
app.logger.warning(f'Could not read notification status cache for {encrypt_text(username)}: {exc}')
return None, version_tag
now = time.time()
@@ -1076,7 +1076,7 @@ def _set_cached_unread_status(username, is_admin, version_tag, payload):
cache_client.setex(key, NOTIFICATION_STATUS_CACHE_TTL, json.dumps(payload, default=str))
return
except Exception as exc:
app.logger.warning(f'Could not write notification status cache for {username}: {exc}')
app.logger.warning(f'Could not write notification status cache for {encrypt_text(username)}: {exc}')
with _NOTIFICATION_CACHE_LOCK:
_NOTIFICATION_LOCAL_CACHE[key] = {
@@ -2144,7 +2144,7 @@ def _upload_student_cards_excel():
created_total += 1
except Exception as exc:
app.logger.error(f'Error importing student cards from Excel: {exc}')
flash(f'Fehler beim Import der Bibliotheksausweise: {exc}', 'error')
flash(f'Fehler beim Import der Bibliotheksausweise', 'error')
return redirect(url_for('student_cards_admin'))
finally:
client.close()
@@ -3442,7 +3442,7 @@ def api_library_scan_action():
'channel': 'library_scan',
'item_id': item_id,
'item_name': item_doc.get('Name', ''),
'borrower': borrower_name,
'borrower':borrower_name,
'student_card_id': student_card_id,
'borrow_duration_days': borrow_duration_days,
}
@@ -3611,7 +3611,6 @@ def api_item_detail(item_id):
"""
client.close()
return detail_html, 200
return detail_html, 200
except Exception as e:
app.logger.error(f"Error fetching item detail: {e}")
return jsonify({'error': str(e)}), 500
@@ -4421,19 +4420,17 @@ def login():
ctx = get_tenant_context()
current_tenant_id = ctx.tenant_id if ctx else None
current_tenant_db = ctx.db_name if ctx else cfg.MONGODB_DB
app.logger.info(f"Login attempt: username={username!r} tenant={current_tenant_id or 'default'} db={current_tenant_db} host={request.host} ip={request.remote_addr}")
app.logger.info(f"Debug login context: headers={dict(request.headers)} tenant_config={ctx.config if ctx else None} remote_addr={request.remote_addr} host={request.host}")
app.logger.info(f"Raw login payload: username={username!r} password={password!r}")
app.logger.info(f"Login attempt: username={encrypt_text(username)!r} tenant={current_tenant_id or 'default'} db={current_tenant_db} host={request.host} ip={encrypt_text(request.remote_addr)}")
app.logger.info(f"Active MongoDB config: uri={getattr(cfg, 'MONGODB_URI', None)!r} host={cfg.MONGODB_HOST!r} port={cfg.MONGODB_PORT!r} default_db={cfg.MONGODB_DB!r}")
if not username or not password:
app.logger.warning(f"Login blocked: missing credentials tenant={current_tenant_id or 'default'} host={request.host} ip={request.remote_addr}")
app.logger.warning(f"Login blocked: missing credentials tenant={current_tenant_id or 'default'} host={request.host} ip={encrypt_text(request.remote_addr)}")
flash('Bitte alle Felder ausfüllen', 'error')
return redirect(url_for('login'))
user = us.check_nm_pwd(username, password)
if user:
app.logger.info(f"Login success: username={username!r} tenant={current_tenant_id or 'default'} db={current_tenant_db} host={request.host} ip={request.remote_addr}")
app.logger.info(f"Login success: username={encrypt_text(username)!r} tenant={current_tenant_id or 'default'} db={current_tenant_db} host={request.host} ip={encrypt_text(request.remote_addr)}")
session['username'] = username
is_admin_user = bool(user.get('Admin', False))
session['admin'] = is_admin_user
@@ -4454,7 +4451,7 @@ def login():
else:
return redirect(url_for('home'))
else:
app.logger.warning(f"Login failed: username={username!r} tenant={current_tenant_id or 'default'} db={current_tenant_db} host={request.host} ip={request.remote_addr}")
app.logger.warning(f"Login failed: username={encrypt_text(username)!r} tenant={current_tenant_id or 'default'} db={current_tenant_db} host={request.host} ip={encrypt_text(request.remote_addr)}")
flash('Ungültige Anmeldedaten', 'error')
get_flashed_messages()
return render_template('login.html')
@@ -5118,7 +5115,7 @@ def upload_item():
# Log mobile request for debugging
if is_mobile:
app.logger.info(f"Mobile upload from {request.headers.get('User-Agent', 'unknown')} by {username}")
app.logger.info(f"Mobile upload from {request.headers.get('User-Agent', 'unknown')} by {encrypt_text(username)}")
try:
# Strip whitespace from all text fields
@@ -5351,7 +5348,7 @@ def upload_item():
# Create a structured log entry for upload session
upload_session_id = str(uuid.uuid4())[:8]
app.logger.info(f"Starting image upload session {upload_session_id} - Files: {len(images)}, User: {username}")
app.logger.info(f"Starting image upload session {upload_session_id} - Files: {len(images)}, User: {encrypt_text(username)}")
# Ensure all required directories exist
for directory in [app.config['UPLOAD_FOLDER']]:
@@ -6105,7 +6102,7 @@ def duplicate_item():
# Log mobile duplication for debugging
if is_mobile:
app.logger.info(f"Mobile duplication from {request.headers.get('User-Agent', 'unknown')} by {username}")
app.logger.info(f"Mobile duplication from {request.headers.get('User-Agent', 'unknown')} by {encrypt_text(username)}")
# Get original item ID
original_item_id = request.form.get('original_item_id')
@@ -9988,11 +9985,6 @@ def my_borrowed_items():
'Status': 'planned'
}))
# DEBUG: Log the number of planned appointments found
app.logger.info(f"Found {len(planned_ausleihungen)} planned appointments for user {username}")
for appt in planned_ausleihungen:
app.logger.info(f"Planned appointment: ID={str(appt['_id'])}, Item={str(appt.get('Item'))}, Start={appt.get('Start')}")
# Process items
active_items = []
planned_items = []
@@ -10198,7 +10190,7 @@ def mark_all_notifications_read():
if result.modified_count > 0:
_bump_notification_version(f'user:{username}')
except Exception as exc:
app.logger.warning(f"Could not mark all notifications as read for {username}: {exc}")
app.logger.warning(f"Could not mark all notifications as read for {encrypt_text(username)}: {exc}")
finally:
if client:
client.close()
@@ -10277,7 +10269,7 @@ def notifications_unread_status():
etag_value = _build_unread_status_etag(version_tag, payload)
return _build_cached_json_response(payload, etag_value)
except Exception as exc:
app.logger.warning(f"Could not fetch unread notification status for {username}: {exc}")
app.logger.warning(f"Could not fetch unread notification status for {encrypt_text(username)}: {exc}")
return jsonify({'ok': False, 'error': 'status_fetch_failed'}), 500
finally:
if client:
@@ -11573,10 +11565,10 @@ def log_mobile_action(action, request, success=True, details=None):
if details:
message += f" - Details: {details}"
if success:
app.logger.info(message)
else:
app.logger.error(message)
# if success:
# app.logger.info(message)
# else:
# app.logger.error(message)
# Add explicit static file routes to handle CSS serving issues
@app.route('/static/<path:filename>')
@@ -11858,7 +11850,7 @@ def subscribe_to_push():
success = pn.save_push_subscription(username, subscription)
if success:
app.logger.info(f'Push subscription saved for {username}')
app.logger.info(f'Push subscription saved for {encrypt_text(username)}')
return jsonify({
'success': True,
'message': 'Successfully subscribed to push notifications'
@@ -11895,7 +11887,7 @@ def unsubscribe_from_push():
success = pn.remove_push_subscription(username, endpoint)
if success:
app.logger.info(f'Push subscription removed for {username}')
app.logger.info(f'Push subscription removed for {encrypt_text(username)}')
return jsonify({
'success': True,
'message': 'Successfully unsubscribed from push notifications'
+2 -3
View File
@@ -21,7 +21,7 @@ def log_status_change(ausleihung_id, old_status, new_status, user=None):
ausleihung_id: Die ID der Ausleihung
old_status: Der alte Status
new_status: Der neue Status
user: Der Benutzer, der die Änderung vorgenommen hat (optional)
"""
try:
# Erstelle Log-Verzeichnis, falls es nicht existiert
@@ -34,10 +34,9 @@ def log_status_change(ausleihung_id, old_status, new_status, user=None):
# Protokolliere die Änderung
timestamp = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
user_info = f" by {user}" if user else ""
with open(log_file, 'a', encoding='utf-8') as f:
f.write(f"{timestamp}: Ausleihung {ausleihung_id} - Status changed from '{old_status}' to '{new_status}'{user_info}\n")
f.write(f"{timestamp}: Ausleihung {ausleihung_id} - Status changed from '{old_status}' to '{new_status}'\n")
return True
except Exception as e:
+69 -108
View File
@@ -13,6 +13,7 @@ import logging
import Web.modules.database.settings as cfg
from Web.modules.database.settings import MongoClient
from Web.modules.inventarsystem.data_protection import encrypt_text, decrypt_text
logger = logging.getLogger(__name__)
@@ -44,16 +45,22 @@ if not VAPID_PUBLIC_KEY or not VAPID_PRIVATE_KEY:
serialization.PublicFormat.UncompressedPoint
)
VAPID_PUBLIC_KEY = b64urlencode(raw_pub)
VAPID_PUBLIC_KEY = b64urlencode(raw_pub).decode('utf-8')
VAPID_PRIVATE_KEY = VAPID_PRIVATE_PEM
except Exception as e:
logger.error(f'Could not load or generate VAPID keys: {e}')
# Push service endpoint (typically Firebase or Web Push Service)
PUSH_SERVICE_URL = 'https://fcm.googleapis.com/fcm/send' # Firebase Cloud Messaging
FCM_API_KEY = os.getenv('FCM_API_KEY', '') # Firebase API key
def _get_username_hash(username):
"""Generates a deterministic hash for database lookups."""
if not username:
return None
return hashlib.sha256(username.encode('utf-8')).hexdigest()
def get_push_subscriptions_collection(db=None):
"""Get MongoDB push subscriptions collection"""
if db is None:
@@ -64,71 +71,68 @@ def get_push_subscriptions_collection(db=None):
def get_user_subscriptions(username):
"""
Get all active push subscriptions for a user
Args:
username (str): Username
Returns:
list: List of subscription documents
Get all active push subscriptions for a user, decrypting data on the fly.
"""
try:
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB]
subs_col = get_push_subscriptions_collection(db)
subscriptions = list(subs_col.find({
'Username': username,
# Query using the deterministic hash, NOT the encrypted text directly
user_hash = _get_username_hash(username)
encrypted_subscriptions = list(subs_col.find({
'UsernameHash': user_hash,
'IsActive': True
}))
client.close()
return subscriptions
# Decrypt endpoints and keys before returning
decrypted_subs = []
for sub in encrypted_subscriptions:
try:
sub['Endpoint'] = decrypt_text(sub.get('Endpoint'))
# Keys are stored as encrypted JSON strings
decrypted_keys_str = decrypt_text(sub.get('Keys'))
sub['Keys'] = json.loads(decrypted_keys_str) if decrypted_keys_str else {}
decrypted_subs.append(sub)
except Exception as e:
logger.error(f"Failed to decrypt subscription payload for hash {user_hash}: {e}")
return decrypted_subs
except Exception as e:
logger.error(f'Error getting push subscriptions for {username}: {e}')
logger.error(f'Error getting push subscriptions for user: {e}')
return []
def save_push_subscription(username, subscription_obj):
"""
Save a new push subscription for a user
Args:
username (str): Username
subscription_obj (dict): Subscription object from Service Worker
{
'endpoint': 'https://...',
'keys': {
'p256dh': '...',
'auth': '...'
}
}
Returns:
bool: Success status
Save a new push subscription for a user with field-level encryption.
"""
try:
if not subscription_obj.get('endpoint'):
logger.warning(f'Invalid subscription object for {username}')
endpoint = subscription_obj.get('endpoint')
if not endpoint:
logger.warning('Invalid subscription object: missing endpoint')
return False
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB]
subs_col = get_push_subscriptions_collection(db)
# Create unique hash of subscription to avoid duplicates
# Create unique hash of subscription using plaintext data to avoid duplicates
sub_hash = hashlib.shake_256(
f"{username}:{subscription_obj['endpoint']}".encode()
f"{username}:{endpoint}".encode('utf-8')
).hexdigest()
# Check if subscription already exists
# Check if subscription already exists by Hash
existing = subs_col.find_one({
'Username': username,
'SubscriptionHash': sub_hash
})
if existing:
# Update last used time
subs_col.update_one(
{'_id': existing['_id']},
{'$set': {
@@ -136,15 +140,19 @@ def save_push_subscription(username, subscription_obj):
'IsActive': True
}}
)
logger.info(f'Updated existing subscription for {username}')
logger.info('Updated existing push subscription')
client.close()
return True
# Save new subscription
# Format keys as JSON string for your encrypt_text module
keys_str = json.dumps(subscription_obj.get('keys', {}))
# Save new subscription, encrypting sensitive fields
subscription_doc = {
'Username': username,
'Endpoint': subscription_obj['endpoint'],
'Keys': subscription_obj.get('keys', {}),
'UsernameHash': _get_username_hash(username),
'Username': encrypt_text(username),
'Endpoint': encrypt_text(endpoint),
'Keys': encrypt_text(keys_str),
'SubscriptionHash': sub_hash,
'IsActive': True,
'CreatedAt': datetime.datetime.now(),
@@ -153,36 +161,31 @@ def save_push_subscription(username, subscription_obj):
}
subs_col.insert_one(subscription_doc)
logger.info(f'Saved new push subscription for {username}')
logger.info('Saved new encrypted push subscription')
client.close()
return True
except Exception as e:
logger.error(f'Error saving push subscription for {username}: {e}')
logger.error(f'Error saving push subscription: {e}')
return False
def remove_push_subscription(username, endpoint):
"""
Remove a push subscription
Args:
username (str): Username
endpoint (str): Subscription endpoint URL
Returns:
bool: Success status
Remove a push subscription by making it inactive.
"""
try:
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB]
subs_col = get_push_subscriptions_collection(db)
# Recreate the deterministic hash to find the specific subscription
sub_hash = hashlib.shake_256(
f"{username}:{endpoint}".encode('utf-8')
).hexdigest()
result = subs_col.update_one(
{
'Username': username,
'Endpoint': endpoint
},
{'SubscriptionHash': sub_hash},
{'$set': {'IsActive': False}}
)
@@ -190,31 +193,19 @@ def remove_push_subscription(username, endpoint):
return result.modified_count > 0
except Exception as e:
logger.error(f'Error removing push subscription for {username}: {e}')
logger.error(f'Error removing push subscription: {e}')
return False
def send_push_notification(username, title, body, icon=None, url='/', reference=None, tag='notification'):
"""
Send a push notification to all user's subscriptions
Args:
username (str): Target username
title (str): Notification title
body (str): Notification body
icon (str, optional): Icon URL
url (str, optional): URL to open on click
reference (dict, optional): Reference data (item_id, etc)
tag (str, optional): Notification tag for grouping
Returns:
int: Number of successfully sent notifications
Send a push notification to all user's subscriptions.
"""
try:
subscriptions = get_user_subscriptions(username)
if not subscriptions:
logger.debug(f'No active push subscriptions for {username}')
logger.debug('No active push subscriptions for user')
return 0
sent_count = 0
@@ -232,19 +223,18 @@ def send_push_notification(username, title, body, icon=None, url='/', reference=
if success:
sent_count += 1
else:
# Mark subscription as inactive if send fails
_mark_subscription_inactive(subscription['_id'])
logger.info(f'Sent push notification to {username}: {sent_count}/{len(subscriptions)} subscriptions')
logger.info(f'Sent push notification: {sent_count}/{len(subscriptions)} subscriptions')
return sent_count
except Exception as e:
logger.error(f'Error sending push notification to {username}: {e}')
logger.error(f'Error sending push notification: {e}')
return 0
def _send_to_subscription(subscription, title, body, icon, url, reference, tag):
"""Send push notification to a specific subscription"""
"""Send push notification to a specific decrypted subscription"""
try:
payload = {
'title': title,
@@ -256,20 +246,17 @@ def _send_to_subscription(subscription, title, body, icon, url, reference, tag):
'reference': reference or {},
}
# If using Firebase Cloud Messaging
if FCM_API_KEY and subscription.get('Endpoint', '').startswith('https://fcm.'):
return _send_fcm_notification(subscription, payload)
# Otherwise use standard Web Push Protocol
return _send_web_push_notification(subscription, payload)
except Exception as e:
logger.error(f'Error sending to subscription {subscription.get("_id")}: {e}')
logger.error(f'Error sending to subscription: {e}')
return False
def _send_fcm_notification(subscription, payload):
"""Send notification via Firebase Cloud Messaging"""
try:
if not FCM_API_KEY:
logger.warning('FCM_API_KEY not configured')
@@ -311,9 +298,7 @@ def _send_fcm_notification(subscription, payload):
def _send_web_push_notification(subscription, payload):
"""Send notification using standard Web Push Protocol"""
try:
# This requires pywebpush library
from pywebpush import webpush
webpush(
@@ -325,13 +310,13 @@ def _send_web_push_notification(subscription, payload):
vapid_private_key=VAPID_PRIVATE_KEY,
vapid_claims={'sub': VAPID_SUBJECT},
timeout=10,
ttl=3600 # Notification expires after 1 hour if device is offline
ttl=3600
)
return True
except ImportError:
logger.warning('pywebpush not installed, install with: pip install pywebpush')
logger.warning('pywebpush not installed. pip install pywebpush')
return False
except Exception as e:
logger.error(f'Web push error: {e}')
@@ -339,7 +324,6 @@ def _send_web_push_notification(subscription, payload):
def _mark_subscription_inactive(subscription_id):
"""Mark a subscription as inactive (e.g., after failed send)"""
try:
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB]
@@ -349,37 +333,21 @@ def _mark_subscription_inactive(subscription_id):
{'_id': ObjectId(subscription_id)},
{'$set': {'IsActive': False}}
)
client.close()
except Exception as e:
logger.error(f'Error marking subscription inactive: {e}')
def send_push_to_all_admins(title, body, icon=None, url='/', reference=None):
"""
Send a push notification to all admin users
Args:
title (str): Notification title
body (str): Notification body
icon (str, optional): Icon URL
url (str, optional): URL to open on click
reference (dict, optional): Reference data
Returns:
int: Total notifications sent
"""
try:
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB]
users_col = db['users']
# Get all admin users
admin_users = list(users_col.find(
{'Admin': True},
{'Username': 1}
))
client.close()
total_sent = 0
@@ -404,10 +372,6 @@ def send_push_to_all_admins(title, body, icon=None, url='/', reference=None):
def cleanup_inactive_subscriptions():
"""
Remove inactive subscriptions older than 30 days
Run this periodically as a maintenance task
"""
try:
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB]
@@ -429,18 +393,15 @@ def cleanup_inactive_subscriptions():
return 0
# Database collection schema
def ensure_push_subscriptions_collection():
"""Ensure the push_subscriptions collection exists with proper indexes"""
try:
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB]
subs_col = get_push_subscriptions_collection(db)
# Create indexes
subs_col.create_index('Username')
subs_col.create_index([('Username', 1), ('IsActive', 1)])
subs_col.create_index([('CreatedAt', 1)]) # TTL-like usage
subs_col.create_index('UsernameHash')
subs_col.create_index([('UsernameHash', 1), ('IsActive', 1)])
subs_col.create_index([('CreatedAt', 1)])
subs_col.create_index('SubscriptionHash', unique=True)
logger.info('Push subscriptions collection indexes created')