Compare commits

...

9 Commits

5 changed files with 148 additions and 142 deletions
+72 -90
View File
@@ -10375,11 +10375,24 @@ def my_borrowed_items():
@app.route('/api/push/vapid-key', methods=['GET'])
def get_vapid_key():
"""Returns the VAPID public key for web push subscriptions."""
"""
Returns the VAPID public key for web push subscriptions
"""
from Web.push_notifications import _get_vapid_public
if not _get_vapid_public():
return jsonify({'error': 'VAPID public key not configured'}), 500
return jsonify({'publicKey': _get_vapid_public()})
if 'username' not in session:
return jsonify({'success': False, 'error': 'Not authenticated'}), 401
try:
if not _get_vapid_public():
return jsonify({'success': False, 'error': 'VAPID public key not configured'}), 500
return jsonify({
'success': True,
'publicKey': _get_vapid_public()
})
except Exception as e:
app.logger.error(f'Error getting VAPID key: {e}')
return jsonify({'success': False}), 500
@app.route('/notifications')
def notifications_view():
@@ -10404,15 +10417,17 @@ def notifications_view():
admin_notifications = []
for notif in notifications:
is_read = username in (notif.get('ReadBy') or [])
created_at_val = notif.get('CreatedAt')
row = {
'id': str(notif.get('_id')),
'title': notif.get('Title', 'Benachrichtigung'),
'message': notif.get('Message', ''),
'severity': notif.get('Severity', 'info'),
'type': notif.get('Type', ''),
'created_at': notif.get('CreatedAt'),
'created_at': created_at_val if created_at_val else None,
'is_read': is_read,
'reference': notif.get('Reference', {}) or {},
'reference': notif.get('Reference') or {},
}
if notif.get('Audience') == 'admin':
admin_notifications.append(row)
@@ -10466,45 +10481,49 @@ def mark_notification_read(notification_id):
return redirect(url_for('notifications_view'))
@app.route('/notifications/mark_all_read', methods=['POST'])
@app.route('/notifications/mark-all-read', methods=['POST'])
def mark_all_notifications_read():
"""Mark all visible notifications as read for the current user."""
if 'username' not in session:
flash('Bitte melden Sie sich an.', 'error')
return redirect(url_for('login'))
username = session['username']
is_admin_user = False
current_permissions = us.get_effective_permissions(session['username'])
if not current_permissions['actions'].get('can_manage_settings', False):
is_admin_user = True
else:
is_admin_user = False
query = {
'$or': [
{'Audience': 'user', 'TargetUser': username},
]
}
if is_admin_user:
query['$or'].append({'Audience': 'admin'})
current_permissions = us.get_effective_permissions(username)
is_admin_user = current_permissions['actions'].get('can_manage_settings', False)
client = None
try:
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
db = client[MONGODB_DB]
result = db['notifications'].update_many(
# Filter-Logik: Welche Benachrichtigungen sollen als gelesen markiert werden?
# Wenn Sie 'Audience' nutzen (wie in Ihrer notifications_view Route):
query = {}
if is_admin_user:
# Ein Admin sieht sowohl an ihn gerichtete als auch allgemeine Admin-Meldungen
query['$or'] = [
{'TargetUser': username},
{'Audience': 'admin'}
]
else:
# Normale User sehen nur Meldungen, die an sie oder alle User gerichtet sind
query['$or'] = [
{'TargetUser': username},
{'Audience': 'user'}
]
# WICHTIG: Fügt den Benutzernamen per $addToSet in das ReadBy-Array ein.
# $addToSet verhindert, dass der Name doppelt eingetragen wird, falls er schon drinsteht.
db['notifications'].update_many(
query,
{
'$addToSet': {'ReadBy': username},
'$set': {'UpdatedAt': datetime.datetime.now()}
}
{'$addToSet': {'ReadBy': username}}
)
if result.modified_count > 0:
_bump_notification_version(f'user:{username}')
flash('Alle Benachrichtigungen wurden als gelesen markiert.', 'success')
except Exception as exc:
app.logger.warning(f"Could not mark all notifications as read for {encrypt_text(username)}: {exc}")
app.logger.error(f"Error marking all notifications as read: {exc}")
flash('Fehler beim Aktualisieren der Benachrichtigungen.', 'error')
finally:
if client:
client.close()
@@ -12115,44 +12134,33 @@ def get_optimal_image_quality(img, target_size_kb=80):
def health_check():
return 'OK', 200
@app.route('/api/push/subscribe', methods=['POST'])
def subscribe_to_push():
"""
Subscribe a user to push notifications
Expects JSON payload:
{
'subscription': {
'endpoint': '...',
'keys': {'p256dh': '...', 'auth': '...'}
}
}
"""
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:
data = request.get_json() or {}
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)
success = pn.save_push_subscription(username, subscription_obj)
if success:
app.logger.info(f'Push subscription saved for {encrypt_text(username)}')
return jsonify({
'success': True,
'message': 'Successfully subscribed to push notifications'
})
return jsonify({'success': True})
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:
app.logger.error(f'Error subscribing to push: {e}')
return jsonify({'success': False}), 500
app.logger.error(f"Error in subscribe_to_push route: {e}")
return jsonify({'success': False, 'error': 'Internal server error'}), 500
@app.route('/api/push/unsubscribe', methods=['POST'])
@@ -12169,7 +12177,7 @@ def unsubscribe_from_push():
return jsonify({'success': False, 'error': 'Not authenticated'}), 401
try:
data = request.get_json() or {}
data = request.get_json(silent=True) or {}
endpoint = data.get('endpoint')
if not endpoint:
@@ -12225,32 +12233,6 @@ def get_push_subscriptions():
app.logger.error(f'Error getting push subscriptions: {e}')
return jsonify({'success': False}), 500
@app.route('/api/push/vapid-key', methods=['GET'])
def get_vapid_key():
"""
Get the VAPID public key for push notifications
Used by the service worker to communicate with push service
"""
try:
vapid_key = pn.VAPID_PUBLIC_KEY
if not vapid_key:
app.logger.warning('VAPID_PUBLIC_KEY not configured')
return jsonify({
'success': False,
'error': 'Push notifications not configured on server'
}), 501
return jsonify({
'success': True,
'vapid_key': vapid_key
})
except Exception as e:
app.logger.error(f'Error getting VAPID key: {e}')
return jsonify({'success': False}), 500
@app.route('/api/push/test', methods=['POST'])
def test_push_notification():
"""
@@ -12263,7 +12245,7 @@ def test_push_notification():
return jsonify({'success': False, 'error': 'Admin access required'}), 403
try:
data = request.get_json() or {}
data = request.get_json(silent=True) or {}
target_user = data.get('target_user', session['username'])
sent = pn.send_push_notification(
+49 -36
View File
@@ -40,14 +40,18 @@ try:
else:
vapid = Vapid.from_file(VAPID_PRIVATE_PEM)
# Extract public key in standard uncompressed point format encoded in URL-safe base64
raw_pub = vapid.public_key.public_bytes(
serialization.Encoding.X962,
serialization.PublicFormat.UncompressedPoint
)
VAPID_PUBLIC_KEY = b64urlencode(raw_pub).decode('utf-8')
VAPID_PRIVATE_KEY = VAPID_PRIVATE_PEM # pywebpush accepts the file path to the private PEM
encoded_pub = b64urlencode(raw_pub)
if isinstance(encoded_pub, bytes):
VAPID_PUBLIC_KEY = encoded_pub.decode('utf-8')
else:
VAPID_PUBLIC_KEY = encoded_pub
VAPID_PRIVATE_KEY = VAPID_PRIVATE_PEM
except Exception as e:
logger.error(f'Could not load or generate VAPID keys: {e}')
@@ -118,29 +122,33 @@ def get_user_subscriptions(username):
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:
print("--- DEBUG PUSH PAYLOAD ---")
print(subscription_obj)
endpoint = subscription_obj.get('endpoint')
if not endpoint:
logger.warning('Invalid subscription object: missing endpoint')
keys = subscription_obj.get('keys', {})
if not endpoint or not keys.get('p256dh') or not keys.get('auth'):
print(
f"DEBUG FEHLER: Keys fehlen! Endpoint: {bool(endpoint)}, p256dh: {bool(keys.get('p256dh'))}, auth: {bool(keys.get('auth'))}")
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 using plaintext data to avoid duplicates
sub_hash = hashlib.shake_256(
f"{username}:{endpoint}".encode('utf-8')
).hexdigest()
).hexdigest(32)
# Check if subscription already exists by Hash
existing = subs_col.find_one({
'SubscriptionHash': sub_hash
})
if existing:
subs_col.update_one(
{'_id': existing['_id']},
@@ -152,10 +160,10 @@ def save_push_subscription(username, subscription_obj):
logger.info('Updated existing push subscription')
client.close()
return True
# 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 = {
'UsernameHash': _get_username_hash(username),
@@ -168,39 +176,37 @@ def save_push_subscription(username, subscription_obj):
'LastUsed': datetime.datetime.now(),
'UserAgent': subscription_obj.get('userAgent', ''),
}
subs_col.insert_one(subscription_doc)
logger.info('Saved new encrypted push subscription')
client.close()
return True
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
def remove_push_subscription(username, endpoint):
"""
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()
).hexdigest(32)
result = subs_col.update_one(
{'SubscriptionHash': sub_hash},
{'$set': {'IsActive': False}}
)
client.close()
return result.modified_count > 0
return result.matched_count > 0
except Exception as e:
logger.error(f'Error removing push subscription: {e}')
return False
@@ -308,8 +314,8 @@ def _send_fcm_notification(subscription, payload):
def _send_web_push_notification(subscription, payload):
try:
from pywebpush import webpush
from pywebpush import webpush, WebPushException
webpush(
subscription_info={
'endpoint': subscription['Endpoint'],
@@ -319,18 +325,25 @@ def _send_web_push_notification(subscription, payload):
vapid_private_key=VAPID_PRIVATE_KEY,
vapid_claims={'sub': VAPID_SUBJECT},
timeout=10,
ttl=3600
ttl=3600
)
return True
except ImportError:
logger.warning('pywebpush not installed. pip install pywebpush')
return False
except Exception as e:
logger.error(f'Web push error: {e}')
return False
# HÄRTUNG: Spezifische WebPush-Fehler abfangen
except WebPushException as ex:
# 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):
try:
+2 -1
View File
@@ -16,4 +16,5 @@ openpyxl
cryptography>=42.0.0
pywebpush
py-vapid>=1.9.0
beautifulsoup4
beautifulsoup4
pywebpush
+23 -14
View File
@@ -25,7 +25,7 @@ class PushNotificationManager {
* Initialize push notification system
* Must be called after page load
*/
async init() {
async init(providedKey = null) {
if (!this.isSupported) {
console.log('Push notifications not supported in this browser');
return false;
@@ -49,11 +49,18 @@ class PushNotificationManager {
}
}
// Fetch VAPID public key from server
// Wenn der Key direkt aus dem Template übergeben wurde, nutze diesen (spart einen Request)
if (providedKey) {
this.vapidKey = providedKey;
return true;
}
// Ansonsten: Fetch VAPID public key from server
const keyResponse = await fetch('/api/push/vapid-key');
if (keyResponse.ok) {
const keyData = await keyResponse.json();
this.vapidKey = keyData.vapid_key;
// WICHTIG: Muss exakt mit dem Python-JSON-Key übereinstimmen!
this.vapidKey = keyData.publicKey;
} else {
console.warn('Failed to fetch VAPID key');
return false;
@@ -154,20 +161,22 @@ class PushNotificationManager {
try {
const subscription = await this.serviceWorkerRegistration.pushManager.getSubscription();
if (!subscription) {
console.warn('No active push subscription');
return false;
}
// Remove subscription on server
const success = await this.removeSubscriptionFromServer(subscription);
// Unsubscribe from push service
if (success) {
await subscription.unsubscribe();
return true;
}
return false;
// Best-Effort: Server benachrichtigen (Eigener try/catch Block!)
try {
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 hier wird jetzt garantiert ausgeführt
await subscription.unsubscribe();
return true;
} catch (error) {
console.error('Failed to unsubscribe from push notifications:', error);
return false;
+2 -1
View File
@@ -16,4 +16,5 @@ openpyxl
cryptography>=42.0.0
pywebpush
py-vapid>=1.9.0
beautifulsoup4
beautifulsoup4
pywebpush