Compare commits

...

15 Commits

Author SHA1 Message Date
Aiirondev_dev 6c3d62e84b Fix of a NoneType has no attribute 'get' Error 2026-07-31 20:50:58 +02:00
Aiirondev_dev 88f6c3c0f5 backwarts compaibility and integration of encryption in the manage-tenant.sh 2026-07-31 20:45:16 +02:00
Aiirondev_dev 08d8b78d91 changes to the encryption to the users and Items 2026-07-31 20:16:34 +02:00
Aiirondev_dev 258e287a39 changes to the encryption to the users and Items 2026-07-31 19:55:59 +02:00
Aiirondev_dev 9b12d9a3d0 changes to the encryption to the users and Items 2026-07-31 15:15:09 +02:00
Aiirondev_dev 237788af96 implementation of encryption for the username to avoid any recognission potetioal 2026-07-31 13:12:46 +02:00
Aiirondev_dev 7368d82fc4 changes to fully incorpereate the school info managment back into the working system 2026-07-30 23:55:46 +02:00
Aiirondev_dev 3e5e243ddf changes to the autorisation system for the page autorisation to have a continued line of page authentification and no discrepencies in the Software it self 2026-07-30 23:34:06 +02:00
Aiirondev_dev 8176cea8fe change of the HTML Signature 2026-07-30 16:52:34 +02:00
Aiirondev_dev b71f2e9089 changes 2026-07-30 01:09:58 +02:00
Aiirondev_dev 1331afa4da Fix of hexdigest 2026-07-29 13:40:17 +02:00
Aiirondev_dev 5a2d703bc7 Debug of Vapid Keys 2026-07-29 13:35:15 +02:00
Aiirondev_dev 8e6dd17243 Fix of the notification subscription 2026-07-29 11:57:34 +02:00
Aiirondev_dev 2124ca65b2 Fix of the notification subscription 2026-07-29 11:49:54 +02:00
Aiirondev_dev bc86dc4a0d Fix of the notification subscription 2026-07-29 11:43:18 +02:00
12 changed files with 758 additions and 1190 deletions
+24 -36
View File
@@ -10417,15 +10417,17 @@ def notifications_view():
admin_notifications = [] admin_notifications = []
for notif in notifications: for notif in notifications:
is_read = username in (notif.get('ReadBy') or []) is_read = username in (notif.get('ReadBy') or [])
created_at_val = notif.get('CreatedAt')
row = { row = {
'id': str(notif.get('_id')), 'id': str(notif.get('_id')),
'title': notif.get('Title', 'Benachrichtigung'), 'title': notif.get('Title', 'Benachrichtigung'),
'message': notif.get('Message', ''), 'message': notif.get('Message', ''),
'severity': notif.get('Severity', 'info'), 'severity': notif.get('Severity', 'info'),
'type': notif.get('Type', ''), 'type': notif.get('Type', ''),
'created_at': notif.get('CreatedAt'), 'created_at': created_at_val if created_at_val else None,
'is_read': is_read, 'is_read': is_read,
'reference': notif.get('Reference', {}) or {}, 'reference': notif.get('Reference') or {},
} }
if notif.get('Audience') == 'admin': if notif.get('Audience') == 'admin':
admin_notifications.append(row) admin_notifications.append(row)
@@ -12132,44 +12134,33 @@ def get_optimal_image_quality(img, target_size_kb=80):
def health_check(): def health_check():
return 'OK', 200 return 'OK', 200
@app.route('/api/push/subscribe', methods=['POST']) @app.route('/api/push/subscribe', methods=['POST'])
def subscribe_to_push(): def subscribe_to_push():
"""
Subscribe a user to push notifications
Expects JSON payload:
{
'subscription': {
'endpoint': '...',
'keys': {'p256dh': '...', 'auth': '...'}
}
}
"""
if 'username' not in session: if 'username' not in session:
return jsonify({'success': False, 'error': 'Not authenticated'}), 401 return jsonify({'success': False, 'error': 'Unauthorized'}), 401
data = request.get_json(silent=True) or {}
subscription_obj = data.get('subscription') if 'subscription' in data else data
if not subscription_obj or not isinstance(subscription_obj, dict):
app.logger.error("Invalid subscription payload received")
return jsonify({'success': False, 'error': 'Invalid payload'}), 400
username = session['username']
try: try:
data = request.get_json(silent=True) or {} success = pn.save_push_subscription(username, subscription_obj)
subscription = data.get('subscription')
if not subscription or not subscription.get('endpoint'):
return jsonify({'success': False, 'error': 'Invalid subscription'}), 400
username = session['username']
success = pn.save_push_subscription(username, subscription)
if success: if success:
app.logger.info(f'Push subscription saved for {encrypt_text(username)}') return jsonify({'success': True})
return jsonify({
'success': True,
'message': 'Successfully subscribed to push notifications'
})
else: else:
return jsonify({'success': False, 'error': 'Failed to save subscription'}), 500 return jsonify({'success': False, 'error': 'Failed to save in DB'}), 400
except Exception as e: except Exception as e:
app.logger.error(f'Error subscribing to push: {e}') app.logger.error(f"Error in subscribe_to_push route: {e}")
return jsonify({'success': False}), 500 return jsonify({'success': False, 'error': 'Internal server error'}), 500
@app.route('/api/push/unsubscribe', methods=['POST']) @app.route('/api/push/unsubscribe', methods=['POST'])
@@ -12250,9 +12241,6 @@ def test_push_notification():
if 'username' not in session: if 'username' not in session:
return jsonify({'success': False, 'error': 'Not authenticated'}), 401 return jsonify({'success': False, 'error': 'Not authenticated'}), 401
if not us.is_admin(session['username']):
return jsonify({'success': False, 'error': 'Admin access required'}), 403
try: try:
data = request.get_json(silent=True) or {} data = request.get_json(silent=True) or {}
target_user = data.get('target_user', session['username']) target_user = data.get('target_user', session['username'])
File diff suppressed because it is too large Load Diff
+205 -173
View File
@@ -19,11 +19,61 @@ Collection Structure:
- Status fields: Verfuegbar, User (if currently borrowed) - Status fields: Verfuegbar, User (if currently borrowed)
""" """
from bson.objectid import ObjectId from bson.objectid import ObjectId
from bson.errors import InvalidId
import datetime import datetime
import Web.modules.database.settings as cfg import Web.modules.database.settings as cfg
from Web.modules.database.settings import MongoClient from Web.modules.database.settings import MongoClient
import Web.modules.inventarsystem.data_protection as dp
def safe_decrypt_user(encrypted_user):
"""
Safely decrypt an encrypted username string.
Returns the original string if decryption fails or if input is empty/None.
"""
if not encrypted_user:
return encrypted_user
try:
return dp.decrypt_text(encrypted_user)
except Exception as e:
print(f"Error decrypting user data: {e}")
# Return fallback value or None to prevent downstream crashes
return "[Decryption Failed]"
def decrypt_item_user_data(item):
"""
Decrypts encrypted user fields within an inventory item document in-place.
Args:
item (dict): MongoDB document representing an item.
Returns:
dict: The item with decrypted user fields.
"""
if not item:
return item
# 1. Decrypt top-level 'User' field if present
if 'User' in item and item['User']:
item['User'] = safe_decrypt_user(item['User'])
# 2. Decrypt nested 'user' field in 'NextAppointment' if present
if 'NextAppointment' in item and isinstance(item['NextAppointment'], dict):
if 'user' in item['NextAppointment']:
item['NextAppointment']['user'] = safe_decrypt_user(item['NextAppointment']['user'])
return item
def _to_object_id(id_str):
"""Safely convert a string to ObjectId."""
try:
return ObjectId(id_str)
except (InvalidId, TypeError):
return None
LIBRARY_ITEM_TYPES = ('book', 'cd', 'dvd', 'other', 'schoolbook', 'Buch', 'Schulbuch', 'schulbuch') LIBRARY_ITEM_TYPES = ('book', 'cd', 'dvd', 'other', 'schoolbook', 'Buch', 'Schulbuch', 'schulbuch')
@@ -52,7 +102,7 @@ def add_item(name, ort, beschreibung, images=None, filter=None, filter2=None, fi
isbn=None, item_type='general', library_category=None, is_library=False): isbn=None, item_type='general', library_category=None, is_library=False):
""" """
Add a new item to the inventory. Add a new item to the inventory.
Args: Args:
name (str): Name of the item name (str): Name of the item
ort (str): Location of the item ort (str): Location of the item
@@ -73,7 +123,7 @@ def add_item(name, ort, beschreibung, images=None, filter=None, filter2=None, fi
isbn (str, optional): ISBN for books or media items isbn (str, optional): ISBN for books or media items
item_type (str, optional): Type of the item (e.g., 'general', 'book', 'cd') item_type (str, optional): Type of the item (e.g., 'general', 'book', 'cd')
library_category (str, optional): Library category for the item library_category (str, optional): Library category for the item
Returns: Returns:
ObjectId: ID of the new item or None if failed ObjectId: ID of the new item or None if failed
""" """
@@ -102,7 +152,7 @@ def add_item(name, ort, beschreibung, images=None, filter=None, filter2=None, fi
'ISBN': isbn, 'ISBN': isbn,
'library_category': library_category, 'library_category': library_category,
'ItemType': item_type, 'ItemType': item_type,
'is_library': is_library, 'is_library': is_library,
'SeriesGroupId': series_group_id, 'SeriesGroupId': series_group_id,
'SeriesCount': series_count, 'SeriesCount': series_count,
'SeriesPosition': series_position, 'SeriesPosition': series_position,
@@ -124,10 +174,10 @@ def add_item(name, ort, beschreibung, images=None, filter=None, filter2=None, fi
def remove_item(id): def remove_item(id):
""" """
Soft-delete an item from the inventory. Soft-delete an item from the inventory.
Args: Args:
id (str): ID of the item to remove id (str): ID of the item to remove
Returns: Returns:
bool: True if successful, False otherwise bool: True if successful, False otherwise
""" """
@@ -199,21 +249,19 @@ def get_group_item_ids(id):
return [] return []
def update_item(id, name, ort, beschreibung, images, verfuegbar, filter1, filter2, filter3, def update_item(id, name, ort, beschreibung, images, verfuegbar, filter1, filter2, filter3,
ansch_jahr, ansch_kost, code_4, reservierbar, isbn=None, item_type='general'): ansch_jahr, ansch_kost, code_4, reservierbar, isbn=None, item_type='general'):
try: try:
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB] db = client[cfg.MONGODB_DB]
items = db['items'] items = db['items']
# 1. Altes Item laden, um SeriesGroupId zu bestimmen
old_item = items.find_one({'_id': ObjectId(id)}) old_item = items.find_one({'_id': ObjectId(id)})
if not old_item: if not old_item:
return False return False
series_group_id = old_item.get('SeriesGroupId') series_group_id = old_item.get('SeriesGroupId')
# 2. Shared Data: Daten, die für ALLE in der Gruppe gleich sind
shared_update = { shared_update = {
'Name': name, 'Name': name,
'Ort': ort, 'Ort': ort,
@@ -227,18 +275,15 @@ def update_item(id, name, ort, beschreibung, images, verfuegbar, filter1, filter
'Reservierbar': reservierbar, 'Reservierbar': reservierbar,
'ISBN': isbn, 'ISBN': isbn,
'ItemType': item_type, 'ItemType': item_type,
'Verfuegbar': verfuegbar, # Wir behalten den Status bei 'Verfuegbar': verfuegbar,
'LastUpdated': datetime.datetime.now() 'LastUpdated': datetime.datetime.now()
} }
# 3. Spezifische Daten: Was NICHT synchronisiert wird
specific_update = shared_update.copy() specific_update = shared_update.copy()
specific_update['Code_4'] = code_4 specific_update['Code_4'] = code_4
# 4. Das aktuelle Item updaten
items.update_one({'_id': ObjectId(id)}, {'$set': specific_update}) items.update_one({'_id': ObjectId(id)}, {'$set': specific_update})
# 5. Alle anderen Gruppen-Mitglieder synchronisieren
if series_group_id: if series_group_id:
items.update_many( items.update_many(
{ {
@@ -257,12 +302,12 @@ def update_item(id, name, ort, beschreibung, images, verfuegbar, filter1, filter
def update_item_status(id, verfuegbar, user=None): def update_item_status(id, verfuegbar, user=None):
""" """
Update the availability status of an inventory item. Update the availability status of an inventory item.
Args: Args:
id (str): ID of the item to update id (str): ID of the item to update
verfuegbar (bool): New availability status verfuegbar (bool): New availability status
user (str, optional): Username of person who borrowed the item user (str, optional): Username of person who borrowed the item
Returns: Returns:
bool: True if successful, False otherwise bool: True if successful, False otherwise
""" """
@@ -279,7 +324,7 @@ def update_item_status(id, verfuegbar, user=None):
update_query = {'$set': update_data} update_query = {'$set': update_data}
if user is not None: if user is not None:
update_data['User'] = user update_data['User'] = dp.encrypt_text(user)
elif verfuegbar: elif verfuegbar:
# If item is being marked as available, clear the user field # If item is being marked as available, clear the user field
update_query['$unset'] = {'User': ""} update_query['$unset'] = {'User': ""}
@@ -299,11 +344,11 @@ def update_item_status(id, verfuegbar, user=None):
def update_item_exemplare_status(id, exemplare_status): def update_item_exemplare_status(id, exemplare_status):
""" """
Update the exemplar status of an inventory item. Update the exemplar status of an inventory item.
Args: Args:
id (str): ID of the item to update id (str): ID of the item to update
exemplare_status (list): List of status objects for each exemplar exemplare_status (list): List of status objects for each exemplar
Returns: Returns:
bool: True if successful, False otherwise bool: True if successful, False otherwise
""" """
@@ -332,32 +377,32 @@ def update_item_exemplare_status(id, exemplare_status):
def is_code_unique(code_4, exclude_id=None): def is_code_unique(code_4, exclude_id=None):
""" """
Check if a given code is unique (not used by any other item). Check if a given code is unique (not used by any other item).
Args: Args:
code_4 (str): The code to check code_4 (str): The code to check
exclude_id (str, optional): ID of item to exclude from the check (for edit operations) exclude_id (str, optional): ID of item to exclude from the check (for edit operations)
Returns: Returns:
bool: True if code is unique, False if already in use bool: True if code is unique, False if already in use
""" """
if not code_4 or code_4.strip() == "": if not code_4 or code_4.strip() == "":
# Empty codes are not considered unique # Empty codes are not considered unique
return False return False
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB] db = client[cfg.MONGODB_DB]
items = db['items'] items = db['items']
# Build query to find items with this code # Build query to find items with this code
query = {'Code_4': code_4, 'Deleted': {'$ne': True}} query = {'Code_4': code_4, 'Deleted': {'$ne': True}}
# If we're editing an item, exclude it from the uniqueness check # If we're editing an item, exclude it from the uniqueness check
if exclude_id: if exclude_id:
query['_id'] = {'$ne': ObjectId(exclude_id)} query['_id'] = {'$ne': ObjectId(exclude_id)}
# Check if any items with this code exist # Check if any items with this code exist
count = items.count_documents(query) count = items.count_documents(query)
client.close() client.close()
return count == 0 return count == 0
@@ -367,7 +412,7 @@ def is_code_unique(code_4, exclude_id=None):
def get_items(): def get_items():
""" """
Retrieve all inventory items. Retrieve all inventory items.
Returns: Returns:
list: List of all inventory item documents with string IDs list: List of all inventory item documents with string IDs
""" """
@@ -390,7 +435,7 @@ def get_items():
def get_available_items(): def get_available_items():
""" """
Retrieve all available inventory items. Retrieve all available inventory items.
Returns: Returns:
list: List of available inventory item documents with string IDs list: List of available inventory item documents with string IDs
""" """
@@ -413,7 +458,7 @@ def get_available_items():
def get_borrowed_items(): def get_borrowed_items():
""" """
Retrieve all currently borrowed inventory items. Retrieve all currently borrowed inventory items.
Returns: Returns:
list: List of borrowed inventory item documents with string IDs list: List of borrowed inventory item documents with string IDs
""" """
@@ -432,36 +477,36 @@ def get_borrowed_items():
print(f"Error retrieving borrowed items: {e}") print(f"Error retrieving borrowed items: {e}")
return [] return []
def get_item(id, decrypt=True):
"""
Retrieve an inventory item by ID, with optional decryption.
"""
item_id = _to_object_id(id)
if not item_id:
return None
def get_item(id):
"""
Retrieve a specific inventory item by its ID.
Args:
id (str): ID of the item to retrieve
Returns:
dict: The inventory item document or None if not found
"""
try: try:
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB] db = client[cfg.MONGODB_DB]
items = db['items'] items = db['items']
item = items.find_one(_active_record_query({'_id': ObjectId(id)})) query = _active_record_query({'_id': item_id})
client.close() item = items.find_one(query)
if item:
item['_id'] = str(item['_id'])
if decrypt:
decrypt_item_user_data(item)
return item return item
except Exception as e: except Exception as e:
print(f"Error retrieving item: {e}") print(f"Error retrieving item {id}: {e}")
return None return None
def get_item_by_name(name): def get_item_by_name(name):
""" """
Retrieve a specific inventory item by its name. Retrieve a specific inventory item by its name.
Args: Args:
name (str): Name of the item to retrieve name (str): Name of the item to retrieve
Returns: Returns:
dict: The inventory item document or None if not found dict: The inventory item document or None if not found
""" """
@@ -480,10 +525,10 @@ def get_item_by_name(name):
def get_items_by_filter(filter_value): def get_items_by_filter(filter_value):
""" """
Retrieve inventory items matching a specific filter/category. Retrieve inventory items matching a specific filter/category.
Args: Args:
filter_value (str): Filter value to search for filter_value (str): Filter value to search for
Returns: Returns:
list: List of items matching the filter in primary, secondary, or tertiary category list: List of items matching the filter in primary, secondary, or tertiary category
""" """
@@ -491,7 +536,7 @@ def get_items_by_filter(filter_value):
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB] db = client[cfg.MONGODB_DB]
items = db['items'] items = db['items']
# Use $or to find matches in any filter field # Use $or to find matches in any filter field
query = _active_record_query(_non_library_query({ query = _active_record_query(_non_library_query({
'$or': [ '$or': [
@@ -500,14 +545,14 @@ def get_items_by_filter(filter_value):
{'Filter3': filter_value} {'Filter3': filter_value}
] ]
})) }))
results = list(items.find(query)) results = list(items.find(query))
client.close() client.close()
# Convert ObjectId to string # Convert ObjectId to string
for item in results: for item in results:
item['_id'] = str(item['_id']) item['_id'] = str(item['_id'])
return results return results
except Exception as e: except Exception as e:
print(f"Error retrieving items by filter: {e}") print(f"Error retrieving items by filter: {e}")
@@ -517,7 +562,7 @@ def get_items_by_filter(filter_value):
def get_filters(): def get_filters():
""" """
Retrieve all unique filter/category values from the inventory. Retrieve all unique filter/category values from the inventory.
Returns: Returns:
list: Combined list of all primary, secondary and tertiary filter values list: Combined list of all primary, secondary and tertiary filter values
""" """
@@ -529,16 +574,16 @@ def get_filters():
filters = items.distinct('Filter', non_library) filters = items.distinct('Filter', non_library)
filters2 = items.distinct('Filter2', non_library) filters2 = items.distinct('Filter2', non_library)
filters3 = items.distinct('Filter3', non_library) filters3 = items.distinct('Filter3', non_library)
# Combine filters and remove None/empty values # Combine filters and remove None/empty values
all_filters = [f for f in filters + filters2 + filters3 if f] all_filters = [f for f in filters + filters2 + filters3 if f]
# Remove duplicates while preserving order # Remove duplicates while preserving order
unique_filters = [] unique_filters = []
for f in all_filters: for f in all_filters:
if f not in unique_filters: if f not in unique_filters:
unique_filters.append(f) unique_filters.append(f)
client.close() client.close()
return unique_filters return unique_filters
except Exception as e: except Exception as e:
@@ -549,7 +594,7 @@ def get_filters():
def get_primary_filters(): def get_primary_filters():
""" """
Retrieve all unique primary filter values. Retrieve all unique primary filter values.
Returns: Returns:
list: List of all primary filter values list: List of all primary filter values
""" """
@@ -559,7 +604,7 @@ def get_primary_filters():
items = db['items'] items = db['items']
filters = [f for f in items.distinct('Filter', _active_record_query(_non_library_query())) if f] filters = [f for f in items.distinct('Filter', _active_record_query(_non_library_query())) if f]
client.close() client.close()
# Add predefined values # Add predefined values
predefined = get_predefined_filter_values(1) predefined = get_predefined_filter_values(1)
return sorted(list(set(filters + predefined))) return sorted(list(set(filters + predefined)))
@@ -571,7 +616,7 @@ def get_primary_filters():
def get_secondary_filters(): def get_secondary_filters():
""" """
Retrieve all unique secondary filter values. Retrieve all unique secondary filter values.
Returns: Returns:
list: List of all secondary filter values list: List of all secondary filter values
""" """
@@ -581,7 +626,7 @@ def get_secondary_filters():
items = db['items'] items = db['items']
filters = [f for f in items.distinct('Filter2', _active_record_query(_non_library_query())) if f] filters = [f for f in items.distinct('Filter2', _active_record_query(_non_library_query())) if f]
client.close() client.close()
# Add predefined values # Add predefined values
predefined = get_predefined_filter_values(2) predefined = get_predefined_filter_values(2)
return sorted(list(set(filters + predefined))) return sorted(list(set(filters + predefined)))
@@ -593,7 +638,7 @@ def get_secondary_filters():
def get_tertiary_filters(): def get_tertiary_filters():
""" """
Retrieve all unique tertiary filter values. Retrieve all unique tertiary filter values.
Returns: Returns:
list: List of all tertiary filter values list: List of all tertiary filter values
""" """
@@ -603,7 +648,7 @@ def get_tertiary_filters():
items = db['items'] items = db['items']
filters = [f for f in items.distinct('Filter3', _active_record_query(_non_library_query())) if f] filters = [f for f in items.distinct('Filter3', _active_record_query(_non_library_query())) if f]
client.close() client.close()
# Add predefined values # Add predefined values
predefined = get_predefined_filter_values(3) predefined = get_predefined_filter_values(3)
return sorted(list(set(filters + predefined))) return sorted(list(set(filters + predefined)))
@@ -615,10 +660,10 @@ def get_tertiary_filters():
def get_item_by_code_4(code_4): def get_item_by_code_4(code_4):
""" """
Retrieve inventory items matching a specific 4-digit code. Retrieve inventory items matching a specific 4-digit code.
Args: Args:
code_4 (str): 4-digit code to search for code_4 (str): 4-digit code to search for
Returns: Returns:
list: List of items matching the code list: List of items matching the code
""" """
@@ -627,11 +672,11 @@ def get_item_by_code_4(code_4):
db = client[cfg.MONGODB_DB] db = client[cfg.MONGODB_DB]
items = db['items'] items = db['items']
results = list(items.find(_active_record_query(_non_library_query({"Code_4": code_4})))) results = list(items.find(_active_record_query(_non_library_query({"Code_4": code_4}))))
# Convert ObjectId to string # Convert ObjectId to string
for item in results: for item in results:
item['_id'] = str(item['_id']) item['_id'] = str(item['_id'])
client.close() client.close()
return results return results
except Exception as e: except Exception as e:
@@ -645,10 +690,10 @@ def unstuck_item(id):
""" """
Remove all borrowing records for a specific item to reset its status. Remove all borrowing records for a specific item to reset its status.
Used to fix problematic or stuck items. Used to fix problematic or stuck items.
Args: Args:
id (str): ID of the item to unstick id (str): ID of the item to unstick
Returns: Returns:
bool: True if successful, False otherwise bool: True if successful, False otherwise
""" """
@@ -664,7 +709,7 @@ def unstuck_item(id):
'LastUpdated': datetime.datetime.now() 'LastUpdated': datetime.datetime.now()
}} }}
) )
# Also reset the item status # Also reset the item status
items = db['items'] items = db['items']
items.update_one( items.update_one(
@@ -677,7 +722,7 @@ def unstuck_item(id):
'$unset': {'User': ""} '$unset': {'User': ""}
} }
) )
client.close() client.close()
return True return True
except Exception as e: except Exception as e:
@@ -688,24 +733,24 @@ def unstuck_item(id):
def get_predefined_filter_values(filter_num): def get_predefined_filter_values(filter_num):
""" """
Get predefined values for a specific filter. Get predefined values for a specific filter.
Args: Args:
filter_num (int): Filter number (1 for Unterrichtsfach, 2 for Jahrgangsstufe) filter_num (int): Filter number (1 for Unterrichtsfach, 2 for Jahrgangsstufe)
Returns: Returns:
list: List of predefined filter values list: List of predefined filter values
""" """
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB] db = client[cfg.MONGODB_DB]
# Use a dedicated collection for filter presets # Use a dedicated collection for filter presets
filter_presets = db['filter_presets'] filter_presets = db['filter_presets']
# Find the document for the specified filter # Find the document for the specified filter
filter_doc = filter_presets.find_one({'filter_num': filter_num}) filter_doc = filter_presets.find_one({'filter_num': filter_num})
client.close() client.close()
if filter_doc and 'values' in filter_doc: if filter_doc and 'values' in filter_doc:
# Sort values alphabetically # Sort values alphabetically
return sorted(filter_doc['values']) return sorted(filter_doc['values'])
@@ -725,60 +770,60 @@ def get_predefined_filter_values(filter_num):
def add_predefined_filter_value(filter_num, value): def add_predefined_filter_value(filter_num, value):
""" """
Add a new predefined value to a filter. Add a new predefined value to a filter.
Args: Args:
filter_num (int): Filter number (1 for Unterrichtsfach, 2 for Jahrgangsstufe) filter_num (int): Filter number (1 for Unterrichtsfach, 2 for Jahrgangsstufe)
value (str): Value to add value (str): Value to add
Returns: Returns:
bool: True if value was added, False if it already existed bool: True if value was added, False if it already existed
""" """
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB] db = client[cfg.MONGODB_DB]
filter_presets = db['filter_presets'] filter_presets = db['filter_presets']
# Check if value already exists # Check if value already exists
filter_doc = filter_presets.find_one({ filter_doc = filter_presets.find_one({
'filter_num': filter_num, 'filter_num': filter_num,
'values': value 'values': value
}) })
if filter_doc: if filter_doc:
# Value already exists # Value already exists
client.close() client.close()
return False return False
# Add the value to the filter # Add the value to the filter
result = filter_presets.update_one( result = filter_presets.update_one(
{'filter_num': filter_num}, {'filter_num': filter_num},
{'$push': {'values': value}}, {'$push': {'values': value}},
upsert=True upsert=True
) )
client.close() client.close()
return result.modified_count > 0 or result.upserted_id is not None return result.modified_count > 0 or result.upserted_id is not None
def remove_predefined_filter_value(filter_num, value): def remove_predefined_filter_value(filter_num, value):
""" """
Remove a predefined value from a filter. Remove a predefined value from a filter.
Args: Args:
filter_num (int): Filter number (1 for Unterrichtsfach, 2 for Jahrgangsstufe) filter_num (int): Filter number (1 for Unterrichtsfach, 2 for Jahrgangsstufe)
value (str): Value to remove value (str): Value to remove
Returns: Returns:
bool: True if value was removed, False otherwise bool: True if value was removed, False otherwise
""" """
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB] db = client[cfg.MONGODB_DB]
filter_presets = db['filter_presets'] filter_presets = db['filter_presets']
# Remove the value from the filter # Remove the value from the filter
result = filter_presets.update_one( result = filter_presets.update_one(
{'filter_num': filter_num}, {'filter_num': filter_num},
{'$pull': {'values': value}} {'$pull': {'values': value}}
) )
client.close() client.close()
return result.modified_count > 0 return result.modified_count > 0
@@ -786,45 +831,45 @@ def remove_predefined_filter_value(filter_num, value):
def edit_predefined_filter_value(filter_num, old_value, new_value): def edit_predefined_filter_value(filter_num, old_value, new_value):
""" """
Edit a predefined value from a filter and update all matching items. Edit a predefined value from a filter and update all matching items.
Args: Args:
filter_num (int): Filter number (1 for Unterrichtsfach, 2 for Jahrgangsstufe) filter_num (int): Filter number (1 for Unterrichtsfach, 2 for Jahrgangsstufe)
old_value (str): Value to replace old_value (str): Value to replace
new_value (str): New value new_value (str): New value
Returns: Returns:
bool: True if value was updated, False otherwise bool: True if value was updated, False otherwise
""" """
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB] db = client[cfg.MONGODB_DB]
filter_presets = db['filter_presets'] filter_presets = db['filter_presets']
# Check if the new value already exists # Check if the new value already exists
existing = filter_presets.find_one({ existing = filter_presets.find_one({
'filter_num': filter_num, 'filter_num': filter_num,
'values': new_value 'values': new_value
}) })
if existing and old_value != new_value: if existing and old_value != new_value:
client.close() client.close()
return False return False
# Update the value in the filter # Update the value in the filter
result = filter_presets.update_one( result = filter_presets.update_one(
{'filter_num': filter_num, 'values': old_value}, {'filter_num': filter_num, 'values': old_value},
{'$set': {'values.$': new_value}} {'$set': {'values.$': new_value}}
) )
if result.modified_count > 0: if result.modified_count > 0:
items = db['items'] items = db['items']
filter_field = 'Filter' if filter_num == 1 else f'Filter{filter_num}' filter_field = 'Filter' if filter_num == 1 else f'Filter{filter_num}'
# Also update all items that use this filter # Also update all items that use this filter
items.update_many( items.update_many(
{filter_field: old_value}, {filter_field: old_value},
{'$set': {filter_field: new_value}} {'$set': {filter_field: new_value}}
) )
client.close() client.close()
return result.modified_count > 0 return result.modified_count > 0
@@ -862,22 +907,22 @@ def set_filter_name(filter_num, name):
def get_predefined_locations(): def get_predefined_locations():
""" """
Get list of all predefined locations/placement options. Get list of all predefined locations/placement options.
Returns: Returns:
list: List of predefined location strings list: List of predefined location strings
""" """
try: try:
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB] db = client[cfg.MONGODB_DB]
# Check if settings collection exists, create if not # Check if settings collection exists, create if not
if 'settings' not in db.list_collection_names(): if 'settings' not in db.list_collection_names():
db.create_collection('settings') db.create_collection('settings')
# Get settings document or create if it doesn't exist # Get settings document or create if it doesn't exist
settings_collection = db['settings'] settings_collection = db['settings']
location_settings = settings_collection.find_one({'setting_type': 'predefined_locations'}) location_settings = settings_collection.find_one({'setting_type': 'predefined_locations'})
if not location_settings: if not location_settings:
# Create default settings document if it doesn't exist # Create default settings document if it doesn't exist
settings_collection.insert_one({ settings_collection.insert_one({
@@ -885,12 +930,12 @@ def get_predefined_locations():
'locations': [] 'locations': []
}) })
return [] return []
# Return the predefined locations # Return the predefined locations
locations = location_settings.get('locations', []) locations = location_settings.get('locations', [])
client.close() client.close()
return sorted(locations) return sorted(locations)
except Exception as e: except Exception as e:
print(f"Error getting predefined locations: {str(e)}") print(f"Error getting predefined locations: {str(e)}")
return [] return []
@@ -899,28 +944,28 @@ def get_predefined_locations():
def add_predefined_location(location): def add_predefined_location(location):
""" """
Add a new predefined location. Add a new predefined location.
Args: Args:
location (str): Location to add location (str): Location to add
Returns: Returns:
bool: True if added successfully, False if already exists bool: True if added successfully, False if already exists
""" """
if not location or not isinstance(location, str): if not location or not isinstance(location, str):
return False return False
location = location.strip() location = location.strip()
if not location: if not location:
return False return False
try: try:
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB] db = client[cfg.MONGODB_DB]
settings_collection = db['settings'] settings_collection = db['settings']
# Check if settings document exists, create if not # Check if settings document exists, create if not
location_settings = settings_collection.find_one({'setting_type': 'predefined_locations'}) location_settings = settings_collection.find_one({'setting_type': 'predefined_locations'})
if not location_settings: if not location_settings:
# Create with the new location # Create with the new location
settings_collection.insert_one({ settings_collection.insert_one({
@@ -929,22 +974,22 @@ def add_predefined_location(location):
}) })
client.close() client.close()
return True return True
# Check if location already exists (case-insensitive) # Check if location already exists (case-insensitive)
current_locations = location_settings.get('locations', []) current_locations = location_settings.get('locations', [])
if any(loc.lower() == location.lower() for loc in current_locations): if any(loc.lower() == location.lower() for loc in current_locations):
client.close() client.close()
return False return False
# Add the new location # Add the new location
settings_collection.update_one( settings_collection.update_one(
{'setting_type': 'predefined_locations'}, {'setting_type': 'predefined_locations'},
{'$push': {'locations': location}} {'$push': {'locations': location}}
) )
client.close() client.close()
return True return True
except Exception as e: except Exception as e:
print(f"Error adding predefined location: {str(e)}") print(f"Error adding predefined location: {str(e)}")
return False return False
@@ -953,29 +998,29 @@ def add_predefined_location(location):
def remove_predefined_location(location): def remove_predefined_location(location):
""" """
Remove a predefined location. Remove a predefined location.
Args: Args:
location (str): Location to remove location (str): Location to remove
Returns: Returns:
bool: True if removed successfully bool: True if removed successfully
""" """
if not location: if not location:
return False return False
try: try:
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB] db = client[cfg.MONGODB_DB]
settings_collection = db['settings'] settings_collection = db['settings']
result = settings_collection.update_one( result = settings_collection.update_one(
{'setting_type': 'predefined_locations'}, {'setting_type': 'predefined_locations'},
{'$pull': {'locations': location}} {'$pull': {'locations': location}}
) )
client.close() client.close()
return result.modified_count > 0 return result.modified_count > 0
except Exception as e: except Exception as e:
print(f"Error removing predefined location: {str(e)}") print(f"Error removing predefined location: {str(e)}")
return False return False
@@ -984,17 +1029,12 @@ def remove_predefined_location(location):
def update_item_next_appointment(item_id, appointment_data): def update_item_next_appointment(item_id, appointment_data):
""" """
Update an item with information about its next scheduled appointment. Update an item with information about its next scheduled appointment.
Args: Args:
item_id (str): ID of the item to update item_id (str): ID of the item
appointment_data (dict): Appointment information containing: appointment_data (dict or None): Dictionary containing appointment details
- date: Date of the appointment (e.g., user, start_time, end_time) or None to clear it.
- start_period: Start period number
- end_period: End period number
- user: Username who scheduled the appointment
- notes: Optional notes
- appointment_id: ID of the appointment booking
Returns: Returns:
bool: True if successful, False otherwise bool: True if successful, False otherwise
""" """
@@ -1002,35 +1042,33 @@ def update_item_next_appointment(item_id, appointment_data):
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB] db = client[cfg.MONGODB_DB]
items = db['items'] items = db['items']
# Format the appointment data for storage # If clearing the appointment
# Ensure date is a datetime object for MongoDB storage if appointment_data is None:
appointment_date = appointment_data['date'] update_query = {
if isinstance(appointment_date, datetime.date) and not isinstance(appointment_date, datetime.datetime): '$unset': {'NextAppointment': ""},
# Convert date to datetime for MongoDB compatibility '$set': {'LastUpdated': datetime.datetime.now()}
appointment_date = datetime.datetime.combine(appointment_date, datetime.time()) }
else:
next_appointment = { # Create a copy so we don't mutate the original dictionary passed in
'date': appointment_date, data_to_save = appointment_data.copy()
'end_date': appointment_data.get('end_date', appointment_date),
'start_period': appointment_data['start_period'], # Encrypt the user field if it exists to match the decryption logic at the top
'end_period': appointment_data['end_period'], if 'user' in data_to_save and data_to_save['user']:
'user': appointment_data['user'], data_to_save['user'] = dp.encrypt_text(data_to_save['user'])
'notes': appointment_data.get('notes', ''),
'appointment_id': appointment_data['appointment_id'], update_query = {
'scheduled_at': datetime.datetime.now() '$set': {
} 'NextAppointment': data_to_save,
'LastUpdated': datetime.datetime.now()
update_data = { }
'NextAppointment': next_appointment, }
'LastUpdated': datetime.datetime.now()
}
result = items.update_one( result = items.update_one(
{'_id': ObjectId(item_id)}, {'_id': ObjectId(item_id)},
{'$set': update_data} update_query
) )
client.close() client.close()
return result.modified_count > 0 return result.modified_count > 0
except Exception as e: except Exception as e:
@@ -1041,10 +1079,10 @@ def update_item_next_appointment(item_id, appointment_data):
def clear_item_next_appointment(item_id): def clear_item_next_appointment(item_id):
""" """
Clear the next appointment information from an item. Clear the next appointment information from an item.
Args: Args:
item_id (str): ID of the item to update item_id (str): ID of the item to update
Returns: Returns:
bool: True if successful, False otherwise bool: True if successful, False otherwise
""" """
@@ -1052,12 +1090,12 @@ def clear_item_next_appointment(item_id):
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB] db = client[cfg.MONGODB_DB]
items = db['items'] items = db['items']
result = items.update_one( result = items.update_one(
{'_id': ObjectId(item_id)}, {'_id': ObjectId(item_id)},
{'$unset': {'NextAppointment': ""}, '$set': {'LastUpdated': datetime.datetime.now()}} {'$unset': {'NextAppointment': ""}, '$set': {'LastUpdated': datetime.datetime.now()}}
) )
client.close() client.close()
return result.modified_count > 0 return result.modified_count > 0
except Exception as e: except Exception as e:
@@ -1068,7 +1106,7 @@ def clear_item_next_appointment(item_id):
def get_items_with_appointments(): def get_items_with_appointments():
""" """
Retrieve all items that have scheduled appointments. Retrieve all items that have scheduled appointments.
Returns: Returns:
list: List of items with NextAppointment field list: List of items with NextAppointment field
""" """
@@ -1076,7 +1114,7 @@ def get_items_with_appointments():
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB] db = client[cfg.MONGODB_DB]
items = db['items'] items = db['items']
items_return = items.find({'NextAppointment': {'$exists': True}, 'Deleted': {'$ne': True}}) items_return = items.find({'NextAppointment': {'$exists': True}, 'Deleted': {'$ne': True}})
items_list = [] items_list = []
for item in items_return: for item in items_return:
@@ -1088,31 +1126,25 @@ def get_items_with_appointments():
print(f"Error retrieving items with appointments: {e}") print(f"Error retrieving items with appointments: {e}")
return [] return []
def get_current_status(item_id): def get_current_status(item_id, decrypt=True):
""" """
Retrieve the current status of an item, including availability and user. Retrieve the current status of an item, decrypting the user field if present.
Args:
item_id (str): ID of the item to check
Returns:
dict: Current status of the item or None if not found
""" """
oid = _to_object_id(item_id)
if not oid:
return None
try: try:
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB] db = client[cfg.MONGODB_DB]
items = db['items'] items = db['items']
item = items.find_one({'_id': oid}, {'Verfuegbar': 1, 'User': 1})
item = items.find_one({'_id': ObjectId(item_id)}, {'Verfuegbar': 1, 'User': 1})
if item: if item:
# Convert ObjectId to string for consistency
item['_id'] = str(item['_id']) item['_id'] = str(item['_id'])
client.close() if decrypt:
decrypt_item_user_data(item)
return item return item
else: return None
client.close()
return None
except Exception as e: except Exception as e:
print(f"Error retrieving current status: {e}") print(f"Error retrieving current status for item {item_id}: {e}")
return None return None
+135 -252
View File
@@ -20,6 +20,7 @@ import string
from bson.objectid import ObjectId from bson.objectid import ObjectId
import Web.modules.database.settings as cfg import Web.modules.database.settings as cfg
from Web.modules.database.settings import MongoClient from Web.modules.database.settings import MongoClient
import Web.modules.inventarsystem.data_protection as dp
import hmac import hmac
import os import os
@@ -109,13 +110,6 @@ def build_username_from_name(first_name, last_name=''):
""" """
Build a deterministic username abbreviation from first and last name. Build a deterministic username abbreviation from first and last name.
Uses 3 letters from each name and stores it lowercase. Uses 3 letters from each name and stores it lowercase.
Args:
first_name (str): First name
last_name (str): Last name (optional)
Returns:
str: Generated username
""" """
alias = build_name_synonym(first_name, last_name) alias = build_name_synonym(first_name, last_name)
return alias.lower() return alias.lower()
@@ -324,7 +318,6 @@ def get_effective_permissions(username):
return build_default_permission_payload('full_access') return build_default_permission_payload('full_access')
preset_key = user.get('PermissionPreset') preset_key = user.get('PermissionPreset')
print(preset_key)
payload = build_default_permission_payload(preset_key) payload = build_default_permission_payload(preset_key)
payload['actions'] = _normalize_bool_map(user.get('ActionPermissions', {}), payload['actions']) payload['actions'] = _normalize_bool_map(user.get('ActionPermissions', {}), payload['actions'])
payload['pages'] = _normalize_bool_map(user.get('PagePermissions', {}), payload['pages']) payload['pages'] = _normalize_bool_map(user.get('PagePermissions', {}), payload['pages'])
@@ -352,10 +345,10 @@ def update_user_permissions(username, preset_key, action_permissions=None, page_
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = _get_tenant_db(client) db = _get_tenant_db(client)
users = db['users'] users = db['users']
result = users.update_one({'Username': username}, {'$set': update_data}) result = users.update_one({'Username': dp.encrypt_text(username)}, {'$set': update_data})
if result.matched_count == 0: if result.matched_count == 0:
result = users.update_one({'username': username}, {'$set': update_data}) result = users.update_one({'username': dp.encrypt_text(username)}, {'$set': update_data})
client.close() client.close()
return result.matched_count > 0 return result.matched_count > 0
@@ -367,7 +360,7 @@ def get_favorites(username):
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = _get_tenant_db(client) db = _get_tenant_db(client)
users = db['users'] users = db['users']
user = users.find_one({'Username': username}) or users.find_one({'username': username}) user = users.find_one({'Username': dp.encrypt_text(username)}) or users.find_one({'username': dp.encrypt_text(username)})
client.close() client.close()
if not user: if not user:
return [] return []
@@ -382,7 +375,7 @@ def add_favorite(username, item_id):
db = _get_tenant_db(client) db = _get_tenant_db(client)
users = db['users'] users = db['users']
users.update_one( users.update_one(
{'$or': [{'Username': username}, {'username': username}]}, {'$or': [{'Username': dp.encrypt_text(username)}, {'username': dp.encrypt_text(username)}]},
{'$addToSet': {'favorites': ObjectId(item_id)}} {'$addToSet': {'favorites': ObjectId(item_id)}}
) )
client.close() client.close()
@@ -397,7 +390,7 @@ def remove_favorite(username, item_id):
db = _get_tenant_db(client) db = _get_tenant_db(client)
users = db['users'] users = db['users']
users.update_one( users.update_one(
{'$or': [{'Username': username}, {'username': username}]}, {'$or': [{'Username': dp.encrypt_text(username)}, {'username': dp.encrypt_text(username)}]},
{'$pull': {'favorites': ObjectId(item_id)}} {'$pull': {'favorites': ObjectId(item_id)}}
) )
client.close() client.close()
@@ -406,16 +399,9 @@ def remove_favorite(username, item_id):
return False return False
def check_password_strength(password): def check_password_strength(password):
""" """
Check if a password meets minimum security requirements. Check if a password meets minimum security requirements.
Args:
password (str): Password to check
Returns:
bool: True if password is strong enough, False otherwise
""" """
if password is None: if password is None:
return False return False
@@ -435,19 +421,15 @@ def check_password_strength(password):
def hashing(password, salt=None): def hashing(password, salt=None):
""" """
Hasht ein Passwort mit scrypt. Hasht ein Passwort mit scrypt.
- Wenn kein Salt übergeben wird, wird ein sicherer, zufälliger Salt generiert (für neue Passwörter).
- Format für neue Hashes: v1$<salt_hex>$<hash_hex>
""" """
password_bytes = password.encode('utf-8') # Explizit UTF-8 für Plattformunabhängigkeit password_bytes = password.encode('utf-8')
if salt is None: if salt is None:
# Neuer Benutzer / Passwortänderung -> Dynamischer Salt
random_salt = os.urandom(16) random_salt = os.urandom(16)
hashed = hashlib.scrypt(password_bytes, salt=random_salt, n=16384, r=8, p=1) hashed = hashlib.scrypt(password_bytes, salt=random_salt, n=16384, r=8, p=1)
return f"v1${random_salt.hex()}${hashed.hex()}" return f"v1${random_salt.hex()}${hashed.hex()}"
else: else:
# Bestehender Benutzer (wird zur Verifizierung aufgerufen)
hashed = hashlib.scrypt(password_bytes, salt=salt, n=16384, r=8, p=1) hashed = hashlib.scrypt(password_bytes, salt=salt, n=16384, r=8, p=1)
return hashed.hex() return hashed.hex()
@@ -455,25 +437,20 @@ def hashing(password, salt=None):
def verify_password(provided_password, stored_password_string): def verify_password(provided_password, stored_password_string):
""" """
Verifiziert ein Passwort gegen einen gespeicherten Hash-String. Verifiziert ein Passwort gegen einen gespeicherten Hash-String.
Unterstützt das alte Format (statischer Salt) und das neue Format (v1$...).
""" """
if not stored_password_string: if not stored_password_string:
return False return False
# Überprüfung für das neue, sichere Format
if stored_password_string.startswith("v1$"): if stored_password_string.startswith("v1$"):
try: try:
_, salt_hex, hash_hex = stored_password_string.split("$") _, salt_hex, hash_hex = stored_password_string.split("$")
salt_bytes = bytes.fromhex(salt_hex) salt_bytes = bytes.fromhex(salt_hex)
# Berechne den Hash des eingegebenen Passworts mit dem extrahierten Salt
calculated_hash = hashing(provided_password, salt=salt_bytes) calculated_hash = hashing(provided_password, salt=salt_bytes)
# Timing-Attack-sicherer Vergleich
return hmac.compare_digest(calculated_hash, hash_hex) return hmac.compare_digest(calculated_hash, hash_hex)
except (ValueError, TypeError): except (ValueError, TypeError):
logger.error("Ungültiges Hash-Format in der Datenbank entdeckt.") logger.error("Ungültiges Hash-Format in der Datenbank entdeckt.")
return False return False
else: else:
# Abwärtskompatibilität: Altes Format mit statischem Salt b'some_salt'
old_static_salt = b'some_salt' old_static_salt = b'some_salt'
calculated_hash = hashing(provided_password, salt=old_static_salt) calculated_hash = hashing(provided_password, salt=old_static_salt)
return hmac.compare_digest(calculated_hash, stored_password_string) return hmac.compare_digest(calculated_hash, stored_password_string)
@@ -494,22 +471,26 @@ def check_nm_pwd(username, password):
try: try:
db = client[db_name] db = client[db_name]
users = db['users'] users = db['users']
query = {'$or': [{'Username': username}, {'username': username}]} query = {'$or': [{'Username': dp.encrypt_text(username)}, {'username': dp.encrypt_text(username)}]}
user_record = users.find_one(query) user_record = users.find_one(query)
if user_record is None: if user_record is None:
logger.warning("Kein Benutzer für %r in DB %r gefunden.", username, db_name) query = {'$or': [{'Username': username}, {'username': username}]}
return None user_record_fallback = users.find_one(query)
if user_record_fallback is None:
logger.warning("Kein Benutzer für %r in DB %r gefunden.", dp.encrypt_text(username), db_name)
return None
else:
user_record = user_record_fallback
stored_password = user_record.get('Password') or user_record.get('password') stored_password = user_record.get('Password') or user_record.get('password')
if not verify_password(password, stored_password): if not verify_password(password, stored_password):
logger.warning("Falsches Passwort für Benutzer %r in DB %r.", username, db_name) logger.warning("Falsches Passwort für Benutzer %r in DB %r.", dp.encrypt_text(username), db_name)
return None return None
# Automatische Migration alter Hashes auf das neue Format if stored_password and not stored_password.startswith("v1$"):
if not stored_password.startswith("v1$"):
users.update_one({'_id': user_record['_id']}, {'$set': {'Password': hashing(password)}}) users.update_one({'_id': user_record['_id']}, {'$set': {'Password': hashing(password)}})
return user_record return user_record
@@ -531,40 +512,35 @@ def add_user(
): ):
""" """
Add a new user to the database. Add a new user to the database.
Args:
username (str): Username for the new user
password (str): Password for the new user
Returns:
bool: True if user was added successfully, False if password was too weak
""" """
if not check_password_strength(password):
return False
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
try: try:
db = _get_tenant_db(client) db = _get_tenant_db(client)
users = db['users'] users = db['users']
if not check_password_strength(password):
return False
permission_defaults = build_default_permission_payload(permission_preset) permission_defaults = build_default_permission_payload(permission_preset)
if isinstance(action_permissions, dict): if isinstance(action_permissions, dict):
for key, value in action_permissions.items(): for key, value in action_permissions.items():
permission_defaults['actions'][str(key)] = bool(value) permission_defaults['actions'][str(key)] = bool(value)
if isinstance(page_permissions, dict): if isinstance(page_permissions, dict):
for key, value in page_permissions.items(): for key, value in page_permissions.items():
permission_defaults['pages'][str(key)] = bool(value) permission_defaults['pages'][str(key)] = bool(value)
if permission_preset == "full_access": safe_name = name.strip() if name else ''
can_admin_preset_based = True safe_last_name = last_name.strip() if last_name else ''
else:
can_admin_preset_based = False
user_doc = { user_doc = {
'Username': username, 'Username': dp.encrypt_text(username),
'Password': hashing(password), 'Password': hashing(password),
'Admin': can_admin_preset_based, 'Admin': (permission_preset == "full_access"),
'active_ausleihung': None, 'active_ausleihung': None,
'name': name.strip() if name else '', 'name': dp.encrypt_text(safe_name) if safe_name else '',
'last_name': last_name.strip() if last_name else '', 'last_name': dp.encrypt_text(safe_last_name) if safe_last_name else '',
'IsStudent': bool(is_student), 'IsStudent': bool(is_student),
'PermissionPreset': permission_defaults['preset'], 'PermissionPreset': permission_defaults['preset'],
'ActionPermissions': permission_defaults['actions'], 'ActionPermissions': permission_defaults['actions'],
@@ -601,7 +577,7 @@ def student_card_exists(student_card_id):
def get_user_by_student_card(student_card_id): def get_user_by_student_card(student_card_id):
"""Return user by student card id or None.""" """Return user dict by student card id or None."""
normalized = normalize_student_card_id(student_card_id) normalized = normalize_student_card_id(student_card_id)
if not normalized: if not normalized:
return None return None
@@ -610,65 +586,44 @@ def get_user_by_student_card(student_card_id):
users = db['student_cards'] users = db['student_cards']
found_user = users.find_one({'SchülerName': normalized}) found_user = users.find_one({'SchülerName': normalized})
client.close() client.close()
# Do not call dp.decrypt_text() here because found_user is a MongoDB dictionary.
return found_user return found_user
def make_admin(username): def make_admin(username):
""" """Grant administrator privileges to a user."""
Grant administrator privileges to a user.
Args:
username (str): Username of the user to promote
Returns:
bool: True if user was promoted successfully
"""
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = _get_tenant_db(client) db = _get_tenant_db(client)
users = db['users'] users = db['users']
result = users.update_one({'Username': username}, {'$set': {'Admin': True}}) result = users.update_one({'Username': dp.encrypt_text(username)}, {'$set': {'Admin': True}})
if result.matched_count == 0: if result.matched_count == 0:
result = users.update_one({'username': username}, {'$set': {'Admin': True}}) result = users.update_one({'username': dp.encrypt_text(username)}, {'$set': {'Admin': True}})
client.close() client.close()
return result.matched_count > 0 return result.matched_count > 0
def remove_admin(username): def remove_admin(username):
""" """Remove administrator privileges from a user."""
Remove administrator privileges from a user.
Args:
username (str): Username of the user to demote
Returns:
bool: True if user was demoted successfully
"""
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = _get_tenant_db(client) db = _get_tenant_db(client)
users = db['users'] users = db['users']
result = users.update_one({'Username': username}, {'$set': {'Admin': False}}) result = users.update_one({'Username': dp.encrypt_text(username)}, {'$set': {'Admin': False}})
if result.matched_count == 0: if result.matched_count == 0:
result = users.update_one({'username': username}, {'$set': {'Admin': False}}) result = users.update_one({'username': dp.encrypt_text(username)}, {'$set': {'Admin': False}})
client.close() client.close()
return result.matched_count > 0 return result.matched_count > 0
def get_user(username): def get_user(username):
""" """Retrieve a specific user by username."""
Retrieve a specific user by username.
Args:
username (str): Username to search for
Returns:
dict: User document or None if not found
"""
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
try: try:
def find_in_db(database_name): def find_in_db(database_name):
db = client[database_name] db = client[database_name]
users = db['users'] users = db['users']
return users.find_one({'Username': username}) or users.find_one({'username': username}) return users.find_one({'Username': dp.encrypt_text(username)}) or users.find_one({'username': dp.encrypt_text(username)})
# Try current tenant first when available
tenant_db, tenant_id = _resolve_request_tenant_db() tenant_db, tenant_id = _resolve_request_tenant_db()
if tenant_db: if tenant_db:
user = find_in_db(tenant_db) user = find_in_db(tenant_db)
@@ -681,7 +636,6 @@ def get_user(username):
) )
return None return None
# Fallback to default configured database
user = find_in_db(cfg.MONGODB_DB) user = find_in_db(cfg.MONGODB_DB)
if user: if user:
return user return user
@@ -692,147 +646,89 @@ def get_user(username):
def check_admin(username): def check_admin(username):
""" """Check if a user has administrator privileges."""
Check if a user has administrator privileges.
Args:
username (str): Username to check
Returns:
bool: True if user is an administrator, False otherwise
"""
user = get_user(username) user = get_user(username)
return bool(user and user.get('Admin', False)) return bool(user and user.get('Admin', False))
def update_active_ausleihung(username, id_item, ausleihung): def update_active_ausleihung(username, id_item, ausleihung):
""" """Update a user's active borrowing record."""
Update a user's active borrowing record.
Args:
username (str): Username of the user
id_item (str): ID of the borrowed item
ausleihung (str): ID of the borrowing record
Returns:
bool: True if successful
"""
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = _get_tenant_db(client) db = _get_tenant_db(client)
users = db['users'] users = db['users']
users.update_one({'Username': username}, {'$set': {'active_ausleihung': {'Item': id_item, 'Ausleihung': ausleihung}}})
result = users.update_one(
{'Username': dp.encrypt_text(username)},
{'$set': {'active_ausleihung': {'Item': id_item, 'Ausleihung': ausleihung}}}
)
if result.matched_count == 0:
users.update_one(
{'username': dp.encrypt_text(username)},
{'$set': {'active_ausleihung': {'Item': id_item, 'Ausleihung': ausleihung}}}
)
client.close() client.close()
return True return True
def get_active_ausleihung(username): def get_active_ausleihung(username):
""" """Get a user's active borrowing record."""
Get a user's active borrowing record.
Args:
username (str): Username of the user
Returns:
dict: Active borrowing information or None
"""
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = _get_tenant_db(client) db = _get_tenant_db(client)
users = db['users'] users = db['users']
user = users.find_one({'Username': username})
return user['active_ausleihung'] user = users.find_one({'Username': dp.encrypt_text(username)}) or users.find_one({'username': dp.encrypt_text(username)})
client.close()
if not user:
return None
return user.get('active_ausleihung')
def has_active_borrowing(username): def has_active_borrowing(username):
""" """Check if a user currently has an active borrowing."""
Check if a user currently has an active borrowing.
Args:
username (str): Username to check
Returns:
bool: True if user has an active borrowing, False otherwise
"""
try: try:
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = _get_tenant_db(client) db = _get_tenant_db(client)
users = db['users'] users = db['users']
user = users.find_one({'username': username}) user = users.find_one({'username': dp.encrypt_text(username)}) or users.find_one({'Username': dp.encrypt_text(username)})
if not user:
user = users.find_one({'Username': username})
if not user:
client.close()
return False
has_active = user.get('active_borrowing', False)
client.close() client.close()
return has_active
if not user:
return False
return user.get('active_borrowing', False)
except Exception as e: except Exception as e:
return False return False
def delete_user(username): def delete_user(username):
""" """Delete a user from the database."""
Delete a user from the database.
Administrative function for removing user accounts.
Args:
username (str): Username of the account to delete
Returns:
bool: True if user was deleted successfully, False otherwise
"""
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = _get_tenant_db(client) db = _get_tenant_db(client)
users = db['users'] users = db['users']
result = users.delete_one({'username': username})
client.close() result = users.delete_one({'username': dp.encrypt_text(username)})
if result.deleted_count == 0: if result.deleted_count == 0:
# Try with different field name result = users.delete_one({'Username': dp.encrypt_text(username)})
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = _get_tenant_db(client) client.close()
users = db['users']
result = users.delete_one({'Username': username})
client.close()
return result.deleted_count > 0 return result.deleted_count > 0
def update_active_borrowing(username, item_id, status): def update_active_borrowing(username, item_id, status):
""" """Update a user's active borrowing status."""
Update a user's active borrowing status.
Args:
username (str): Username of the user
item_id (str): ID of the borrowed item or None if returning
status (bool): True if borrowing, False if returning
Returns:
bool: True if successful, False on error
"""
try: try:
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = _get_tenant_db(client) db = _get_tenant_db(client)
users = db['users'] users = db['users']
result = users.update_one(
{'username': username}, update_data = {'$set': {'active_borrowing': status, 'borrowed_item': item_id if status else None}}
{'$set': {
'active_borrowing': status, result = users.update_one({'username': dp.encrypt_text(username)}, update_data)
'borrowed_item': item_id if status else None
}}
)
if result.matched_count == 0: if result.matched_count == 0:
result = users.update_one( result = users.update_one({'Username': dp.encrypt_text(username)}, update_data)
{'Username': username},
{'$set': {
'active_borrowing': status,
'borrowed_item': item_id if status else None
}}
)
client.close() client.close()
return result.modified_count > 0 return result.modified_count > 0
except Exception as e: except Exception as e:
@@ -840,43 +736,37 @@ def update_active_borrowing(username, item_id, status):
def get_name(username): def get_name(username):
""" """Retrieve the name that is associated with the username."""
Retrieve the name that is assosiated with the username.
Returns:
str: String of name
"""
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = _get_tenant_db(client) db = _get_tenant_db(client)
users = db['users'] users = db['users']
user = users.find_one({'Username': username})
name = user.get("name") user = users.find_one({'Username': dp.encrypt_text(username)}) or users.find_one({'username': dp.encrypt_text(username)})
return name client.close()
if not user or not user.get("name"):
return ""
return dp.decrypt_text(user.get("name"))
def get_last_name(username): def get_last_name(username):
""" """Retrieve the last_name that is associated with the username."""
Retrieve the last_name that is assosiated with the username.
Returns:
str: String of last_name
"""
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = _get_tenant_db(client) db = _get_tenant_db(client)
users = db['users'] users = db['users']
user = users.find_one({'Username': username})
name = user.get("last_name") user = users.find_one({'Username': dp.encrypt_text(username)}) or users.find_one({'username': dp.encrypt_text(username)})
return name client.close()
if not user or not user.get("last_name"):
return ""
return dp.decrypt_text(user.get("last_name"))
def get_all_users(): def get_all_users():
""" """Retrieve all users from the database."""
Retrieve all users from the database.
Administrative function for user management.
Returns:
list: List of all user documents
"""
try: try:
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = _get_tenant_db(client) db = _get_tenant_db(client)
@@ -887,65 +777,58 @@ def get_all_users():
except Exception as e: except Exception as e:
return [] return []
def update_password(username, new_password): def update_password(username, new_password):
""" """Update a user's password with a new one."""
Update a user's password with a new one.
Args:
username (str): Username of the user
new_password (str): New password to set
Returns:
bool: True if password was updated successfully, False otherwise
"""
try: try:
if not check_password_strength(new_password): if not check_password_strength(new_password):
return False return False
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = _get_tenant_db(client) db = _get_tenant_db(client)
users = db['users'] users = db['users']
# Hash the new password
hashed_password = hashing(new_password) hashed_password = hashing(new_password)
# Update the user's password
result = users.update_one( result = users.update_one(
{'Username': username}, {'Username': dp.encrypt_text(username)},
{'$set': {'Password': hashed_password}} {'$set': {'Password': hashed_password}}
) )
if result.matched_count == 0:
result = users.update_one(
{'username': dp.encrypt_text(username)},
{'$set': {'Password': hashed_password}}
)
client.close() client.close()
return result.modified_count > 0 return result.modified_count > 0
except Exception as e: except Exception as e:
print(f"Error updating password: {e}") print(f"Error updating password: {e}")
return False return False
def update_user_name(username, name, last_name): def update_user_name(username, name, last_name):
""" """Update a user's name and last name."""
Update a user's name and last name.
Args:
username (str): Username of the user
name (str): New first name
last_name (str): New last name
Returns:
bool: True if updated successfully, False otherwise
"""
try: try:
name_alias = build_name_synonym(name, last_name)
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = _get_tenant_db(client) db = _get_tenant_db(client)
users = db['users'] users = db['users']
safe_name = dp.encrypt_text(name.strip()) if name else ''
safe_last_name = dp.encrypt_text(last_name.strip()) if last_name else ''
result = users.update_one( result = users.update_one(
{'Username': username}, {'Username': dp.encrypt_text(username)},
{'$set': {'name': name_alias, 'last_name': ''}} {'$set': {'name': safe_name, 'last_name': safe_last_name}}
) )
if result.matched_count == 0:
result = users.update_one(
{'username': dp.encrypt_text(username)},
{'$set': {'name': safe_name, 'last_name': safe_last_name}}
)
client.close() client.close()
return True return True
except Exception as e: except Exception as e:
print(f"Error updating user name: {e}") print(f"Error updating user name: {e}")
return False return False
+2 -2
View File
@@ -37,14 +37,14 @@ def send(email: list | str, subject: str, note: str, sender: str) -> bool:
<tr> <tr>
<td> <td>
<p style="margin:0 0 12px 0;">Mit freundlichen Grüßen</p> <p style="margin:0 0 12px 0;">Mit freundlichen Grüßen</p>
<p style="margin:0;"><strong style="font-size:16px;">Automatisierter Email Verteiler für die Schule: {cfg.get_school_info().get("name")}</strong><br></p> <p style="margin:0;"><strong style="font-size:16px;">Automatisierter Email Verteiler für die Schule: {cfg.get_school_info().get("name")}</strong><br></p><br>
<p style="margin:12px 0 0 0;"><strong>Invario UG</strong><br>Am Sportplatz 10<br>83052 Bruckmühl</p> <p style="margin:12px 0 0 0;"><strong>Invario UG</strong><br>Am Sportplatz 10<br>83052 Bruckmühl</p>
</td> </td>
</tr> </tr>
</table> </table>
""" """
text_content = f"{body_message}\n\nMit freundlichen Grüßen\n{sender}\nInvario UG" text_content = f"{body_message}\n\nMit freundlichen Grüßen\n{sender}\n"
html_content = f""" html_content = f"""
<html> <html>
+2 -2
View File
@@ -252,8 +252,8 @@ def new(date_start: str, date_end: str, time_span: list, slots, slot_length, use
if calendar_link: if calendar_link:
email_body += f"\n\nKalendereintrag: {calendar_link}" email_body += f"\n\nKalendereintrag: {calendar_link}"
if normalized_mail and cfg.EMAIL_ENABLED: #if normalized_mail and cfg.EMAIL_ENABLED:
mail_service.send(normalized_mail, subject, email_body) mail_service.send(normalized_mail, subject, email_body, f"Terminplanungssystem {cfg.SCHOOL_INFO_DEFAULT.get("name")}")
return { return {
'appointment_id': id_str, 'appointment_id': id_str,
+33 -20
View File
@@ -122,29 +122,33 @@ def get_user_subscriptions(username):
def save_push_subscription(username, subscription_obj): def save_push_subscription(username, subscription_obj):
"""Save a new push subscription for a user with field-level encryption.""" import traceback # Hilft uns, Fehler genau zu sehen
try: try:
print("--- DEBUG PUSH PAYLOAD ---")
print(subscription_obj)
endpoint = subscription_obj.get('endpoint') endpoint = subscription_obj.get('endpoint')
keys = subscription_obj.get('keys', {}) keys = subscription_obj.get('keys', {})
if not endpoint or not keys.get('p256dh') or not keys.get('auth'): if not endpoint or not keys.get('p256dh') or not keys.get('auth'):
logger.warning(f'Invalid subscription object for {username}: missing endpoint or keys') print(
f"DEBUG FEHLER: Keys fehlen! Endpoint: {bool(endpoint)}, p256dh: {bool(keys.get('p256dh'))}, auth: {bool(keys.get('auth'))}")
return False return False
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB] db = client[cfg.MONGODB_DB]
subs_col = get_push_subscriptions_collection(db) subs_col = get_push_subscriptions_collection(db)
# Create unique hash of subscription using plaintext data to avoid duplicates # Create unique hash of subscription using plaintext data to avoid duplicates
sub_hash = hashlib.shake_256( sub_hash = hashlib.shake_256(
f"{username}:{endpoint}".encode('utf-8') f"{username}:{endpoint}".encode('utf-8')
).hexdigest() ).hexdigest(32)
# Check if subscription already exists by Hash # Check if subscription already exists by Hash
existing = subs_col.find_one({ existing = subs_col.find_one({
'SubscriptionHash': sub_hash 'SubscriptionHash': sub_hash
}) })
if existing: if existing:
subs_col.update_one( subs_col.update_one(
{'_id': existing['_id']}, {'_id': existing['_id']},
@@ -156,10 +160,10 @@ def save_push_subscription(username, subscription_obj):
logger.info('Updated existing push subscription') logger.info('Updated existing push subscription')
client.close() client.close()
return True return True
# Format keys as JSON string for your encrypt_text module # Format keys as JSON string for your encrypt_text module
keys_str = json.dumps(subscription_obj.get('keys', {})) keys_str = json.dumps(subscription_obj.get('keys', {}))
# Save new subscription, encrypting sensitive fields # Save new subscription, encrypting sensitive fields
subscription_doc = { subscription_doc = {
'UsernameHash': _get_username_hash(username), 'UsernameHash': _get_username_hash(username),
@@ -172,14 +176,16 @@ def save_push_subscription(username, subscription_obj):
'LastUsed': datetime.datetime.now(), 'LastUsed': datetime.datetime.now(),
'UserAgent': subscription_obj.get('userAgent', ''), 'UserAgent': subscription_obj.get('userAgent', ''),
} }
subs_col.insert_one(subscription_doc) subs_col.insert_one(subscription_doc)
logger.info('Saved new encrypted push subscription') logger.info('Saved new encrypted push subscription')
client.close() client.close()
return True return True
except Exception as e: except Exception as e:
logger.error(f'Error saving push subscription: {e}') print(f"DEBUG ABSTURZ in save_push_subscription: {e}")
traceback.print_exc()
return False return False
@@ -191,7 +197,7 @@ def remove_push_subscription(username, endpoint):
sub_hash = hashlib.shake_256( sub_hash = hashlib.shake_256(
f"{username}:{endpoint}".encode('utf-8') f"{username}:{endpoint}".encode('utf-8')
).hexdigest() ).hexdigest(32)
result = subs_col.update_one( result = subs_col.update_one(
{'SubscriptionHash': sub_hash}, {'SubscriptionHash': sub_hash},
@@ -308,8 +314,8 @@ def _send_fcm_notification(subscription, payload):
def _send_web_push_notification(subscription, payload): def _send_web_push_notification(subscription, payload):
try: try:
from pywebpush import webpush from pywebpush import webpush, WebPushException
webpush( webpush(
subscription_info={ subscription_info={
'endpoint': subscription['Endpoint'], 'endpoint': subscription['Endpoint'],
@@ -319,18 +325,25 @@ def _send_web_push_notification(subscription, payload):
vapid_private_key=VAPID_PRIVATE_KEY, vapid_private_key=VAPID_PRIVATE_KEY,
vapid_claims={'sub': VAPID_SUBJECT}, vapid_claims={'sub': VAPID_SUBJECT},
timeout=10, timeout=10,
ttl=3600 ttl=3600
) )
return True return True
except ImportError: except ImportError:
logger.warning('pywebpush not installed. pip install pywebpush') logger.warning('pywebpush not installed. pip install pywebpush')
return False return False
except Exception as e: # HÄRTUNG: Spezifische WebPush-Fehler abfangen
logger.error(f'Web push error: {e}') except WebPushException as ex:
return False # HTTP 404 (Not Found) oder 410 (Gone) bedeuten: Abo existiert nicht mehr
if ex.response is not None and ex.response.status_code in [404, 410]:
logger.info(f"Subscription expired or revoked (Code {ex.response.status_code}). Marking inactive.")
return False # False signalisiert der übergeordneten Funktion, das Abo zu deaktivieren
logger.error(f'Web push failed with code {ex.response.status_code if ex.response else "Unknown"}: {ex}')
return False
except Exception as e:
logger.error(f'Unexpected Web push error: {e}')
return False
def _mark_subscription_inactive(subscription_id): def _mark_subscription_inactive(subscription_id):
try: try:
+2 -1
View File
@@ -16,4 +16,5 @@ openpyxl
cryptography>=42.0.0 cryptography>=42.0.0
pywebpush pywebpush
py-vapid>=1.9.0 py-vapid>=1.9.0
beautifulsoup4 beautifulsoup4
pywebpush
+10 -6
View File
@@ -161,15 +161,19 @@ class PushNotificationManager {
try { try {
const subscription = await this.serviceWorkerRegistration.pushManager.getSubscription(); const subscription = await this.serviceWorkerRegistration.pushManager.getSubscription();
if (!subscription) { if (!subscription) {
console.warn('No active push subscription'); return true;
return true; // Bereits deaktiviert
} }
// Best-Effort: Dem Server Bescheid geben (wir ignorieren absichtlich, // Best-Effort: Server benachrichtigen (Eigener try/catch Block!)
// falls der Server das Abo nicht mehr kennt) try {
await this.removeSubscriptionFromServer(subscription); await this.removeSubscriptionFromServer(subscription);
} catch (serverError) {
// Fehler vom Server ignorieren wir absichtlich.
// Das Skript läuft weiter, statt hier abzubrechen!
console.warn('Server-Abmeldung fehlgeschlagen, lösche lokal trotzdem:', serverError);
}
// WICHTIG: Das Abo im Browser immer zwingend löschen! // WICHTIG: Das hier wird jetzt garantiert ausgeführt
await subscription.unsubscribe(); await subscription.unsubscribe();
return true; return true;
+5 -5
View File
@@ -1148,7 +1148,7 @@
{% if current_permissions.pages.get('tutorial_page', False) %} {% if current_permissions.pages.get('tutorial_page', False) %}
<li><a class="dropdown-item" href="{{ url_for('tutorial_page') }}">Tutorial</a></li> <li><a class="dropdown-item" href="{{ url_for('tutorial_page') }}">Tutorial</a></li>
{% endif %} {% endif %}
{% if current_permissions.pages.get('admin_school_settings', False) %} {% if current_permissions.actions.get('can_manage_settings', False) %}
<li><a class="dropdown-item" href="{{ url_for('admin_school_settings') }}">Schulstammdaten</a></li> <li><a class="dropdown-item" href="{{ url_for('admin_school_settings') }}">Schulstammdaten</a></li>
{% endif %} {% endif %}
{% if current_permissions.actions.get('can_view_logs', False) or current_permissions.pages.get('admin_audit_dashboard', False) %} {% if current_permissions.actions.get('can_view_logs', False) or current_permissions.pages.get('admin_audit_dashboard', False) %}
@@ -1257,7 +1257,7 @@
{% if current_permissions.pages.get('manage_locations', False) %} {% if current_permissions.pages.get('manage_locations', False) %}
<li><a class="dropdown-item" href="{{ url_for('manage_locations') }}">Orte verwalten</a></li> <li><a class="dropdown-item" href="{{ url_for('manage_locations') }}">Orte verwalten</a></li>
{% endif %} {% endif %}
{% if current_permissions.pages.get('admin_school_settings', False) %} {% if current_permissions.actions.get('can_manage_settings', False) %}
<li><a class="dropdown-item" href="{{ url_for('admin_school_settings') }}">Schulstammdaten</a></li> <li><a class="dropdown-item" href="{{ url_for('admin_school_settings') }}">Schulstammdaten</a></li>
{% endif %} {% endif %}
{% if current_permissions.pages.get('admin_borrowings', False) %} {% if current_permissions.pages.get('admin_borrowings', False) %}
@@ -1379,7 +1379,7 @@
<li><a class="dropdown-item" href="{{ url_for('student_cards_admin') }}">Bibliotheksausweis</a></li> <li><a class="dropdown-item" href="{{ url_for('student_cards_admin') }}">Bibliotheksausweis</a></li>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% if current_permissions.pages.get('admin_school_settings', False) %} {% if current_permissions.actions.get('can_manage_settings', False) %}
<li><a class="dropdown-item" href="{{ url_for('admin_school_settings') }}">Schulstammdaten</a></li> <li><a class="dropdown-item" href="{{ url_for('admin_school_settings') }}">Schulstammdaten</a></li>
{% endif %} {% endif %}
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
@@ -1698,7 +1698,7 @@
<option value="Orte verwalten"></option> <option value="Orte verwalten"></option>
{% endif %} {% endif %}
{% if current_permissions.pages.get('admin_school_settings', False) %} {% if current_permissions.actions.get('can_manage_settings', False) %}
<option value="Schulstammdaten"></option> <option value="Schulstammdaten"></option>
{% endif %} {% endif %}
@@ -1823,7 +1823,7 @@
{ label: 'Orte verwalten', keywords: ['orte verwalten', 'orte', 'location'], url: {{ url_for('manage_locations')|tojson }} }, { label: 'Orte verwalten', keywords: ['orte verwalten', 'orte', 'location'], url: {{ url_for('manage_locations')|tojson }} },
{% endif %} {% endif %}
{% if current_permissions.pages.get('admin_school_settings', False) %} {% if current_permissions.actions.get('can_manage_settings', False) %}
{ label: 'Schulstammdaten', keywords: ['schule', 'settings', 'stammdaten', 'school settings'], url: {{ url_for('admin_school_settings')|tojson }} }, { label: 'Schulstammdaten', keywords: ['schule', 'settings', 'stammdaten', 'school settings'], url: {{ url_for('admin_school_settings')|tojson }} },
{% endif %} {% endif %}
+5 -4
View File
@@ -200,6 +200,7 @@ sys.path.insert(0, "/app")
sys.path.insert(0, "/app/Web") sys.path.insert(0, "/app/Web")
from Web.modules.database import settings from Web.modules.database import settings
from pymongo import MongoClient from pymongo import MongoClient
import Web.modules.inventarsystem.data_protection as dp
tenant_id = sys.argv[1].lower() tenant_id = sys.argv[1].lower()
mode = sys.argv[2] mode = sys.argv[2]
@@ -244,14 +245,14 @@ page_permissions = {
"manage_locations": True, "manage_locations": True,
} }
if db.users.count_documents({"Username": "admin"}) == 0: if db.users.count_documents({"Username": dp.encrypt_text("admin")}) == 0:
db.users.insert_one({ db.users.insert_one({
"Username": "admin", "Username": dp.encrypt_text("admin"),
"Password": hashed_pw_string, "Password": hashed_pw_string,
"Admin": True, "Admin": True,
"active_ausleihung": None, "active_ausleihung": None,
"name": "Admin", "name": dp.encrypt_text("Admin"),
"last_name": "User", "last_name": dp.encrypt_text("User"),
"IsStudent": False, "IsStudent": False,
"PermissionPreset": "full_access", "PermissionPreset": "full_access",
"ActionPermissions": action_permissions, "ActionPermissions": action_permissions,
+2 -1
View File
@@ -16,4 +16,5 @@ openpyxl
cryptography>=42.0.0 cryptography>=42.0.0
pywebpush pywebpush
py-vapid>=1.9.0 py-vapid>=1.9.0
beautifulsoup4 beautifulsoup4
pywebpush