Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1331afa4da | |||
| 5a2d703bc7 | |||
| 8e6dd17243 | |||
| 2124ca65b2 | |||
| bc86dc4a0d | |||
| a49ed98ed7 | |||
| 4d9bb62907 | |||
| 8251cd9bfd | |||
| 3ed3148b8a | |||
| 27b265eaf5 | |||
| 3571bb6f6d |
+80
-92
@@ -10373,21 +10373,39 @@ 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
|
||||||
|
"""
|
||||||
|
from Web.push_notifications import _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')
|
@app.route('/notifications')
|
||||||
def notifications_view():
|
def notifications_view():
|
||||||
"""Notification center for users and admins."""
|
"""Notification center for users and admins."""
|
||||||
|
from Web.push_notifications import _get_vapid_public
|
||||||
if 'username' not in session:
|
if 'username' not in session:
|
||||||
flash('Bitte melden Sie sich an, um Benachrichtigungen zu sehen.', 'error')
|
flash('Bitte melden Sie sich an, um Benachrichtigungen zu sehen.', 'error')
|
||||||
return redirect(url_for('login'))
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
username = session['username']
|
username = session['username']
|
||||||
is_admin_user = False
|
|
||||||
current_permissions = us.get_effective_permissions(session['username'])
|
current_permissions = us.get_effective_permissions(session['username'])
|
||||||
|
|
||||||
if not current_permissions['actions'].get('can_manage_settings', False):
|
is_admin_user = current_permissions['actions'].get('can_manage_settings', False)
|
||||||
is_admin_user = True
|
|
||||||
else:
|
|
||||||
is_admin_user = False
|
|
||||||
|
|
||||||
client = None
|
client = None
|
||||||
try:
|
try:
|
||||||
@@ -10399,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)
|
||||||
@@ -10421,6 +10441,7 @@ def notifications_view():
|
|||||||
is_admin_user=is_admin_user,
|
is_admin_user=is_admin_user,
|
||||||
library_module_enabled=cfg.MODULES.is_enabled('library'),
|
library_module_enabled=cfg.MODULES.is_enabled('library'),
|
||||||
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards'),
|
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards'),
|
||||||
|
vapid_public_key=_get_vapid_public()
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
app.logger.error(f"Error loading notifications: {exc}")
|
app.logger.error(f"Error loading notifications: {exc}")
|
||||||
@@ -10460,45 +10481,49 @@ def mark_notification_read(notification_id):
|
|||||||
return redirect(url_for('notifications_view'))
|
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():
|
def mark_all_notifications_read():
|
||||||
"""Mark all visible notifications as read for the current user."""
|
|
||||||
if 'username' not in session:
|
if 'username' not in session:
|
||||||
|
flash('Bitte melden Sie sich an.', 'error')
|
||||||
return redirect(url_for('login'))
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
username = session['username']
|
username = session['username']
|
||||||
is_admin_user = False
|
|
||||||
|
|
||||||
current_permissions = us.get_effective_permissions(session['username'])
|
current_permissions = us.get_effective_permissions(username)
|
||||||
|
is_admin_user = current_permissions['actions'].get('can_manage_settings', False)
|
||||||
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'})
|
|
||||||
|
|
||||||
client = None
|
client = None
|
||||||
try:
|
try:
|
||||||
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
|
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
|
||||||
db = client[MONGODB_DB]
|
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,
|
query,
|
||||||
{
|
{'$addToSet': {'ReadBy': username}}
|
||||||
'$addToSet': {'ReadBy': username},
|
|
||||||
'$set': {'UpdatedAt': datetime.datetime.now()}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
if result.modified_count > 0:
|
|
||||||
_bump_notification_version(f'user:{username}')
|
flash('Alle Benachrichtigungen wurden als gelesen markiert.', 'success')
|
||||||
except Exception as exc:
|
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:
|
finally:
|
||||||
if client:
|
if client:
|
||||||
client.close()
|
client.close()
|
||||||
@@ -12109,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() 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'])
|
||||||
@@ -12163,7 +12177,7 @@ def unsubscribe_from_push():
|
|||||||
return jsonify({'success': False, 'error': 'Not authenticated'}), 401
|
return jsonify({'success': False, 'error': 'Not authenticated'}), 401
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data = request.get_json() or {}
|
data = request.get_json(silent=True) or {}
|
||||||
endpoint = data.get('endpoint')
|
endpoint = data.get('endpoint')
|
||||||
|
|
||||||
if not endpoint:
|
if not endpoint:
|
||||||
@@ -12219,32 +12233,6 @@ def get_push_subscriptions():
|
|||||||
app.logger.error(f'Error getting push subscriptions: {e}')
|
app.logger.error(f'Error getting push subscriptions: {e}')
|
||||||
return jsonify({'success': False}), 500
|
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'])
|
@app.route('/api/push/test', methods=['POST'])
|
||||||
def test_push_notification():
|
def test_push_notification():
|
||||||
"""
|
"""
|
||||||
@@ -12257,7 +12245,7 @@ def test_push_notification():
|
|||||||
return jsonify({'success': False, 'error': 'Admin access required'}), 403
|
return jsonify({'success': False, 'error': 'Admin access required'}), 403
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data = request.get_json() or {}
|
data = request.get_json(silent=True) or {}
|
||||||
target_user = data.get('target_user', session['username'])
|
target_user = data.get('target_user', session['username'])
|
||||||
|
|
||||||
sent = pn.send_push_notification(
|
sent = pn.send_push_notification(
|
||||||
|
|||||||
+52
-36
@@ -40,14 +40,18 @@ try:
|
|||||||
else:
|
else:
|
||||||
vapid = Vapid.from_file(VAPID_PRIVATE_PEM)
|
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(
|
raw_pub = vapid.public_key.public_bytes(
|
||||||
serialization.Encoding.X962,
|
serialization.Encoding.X962,
|
||||||
serialization.PublicFormat.UncompressedPoint
|
serialization.PublicFormat.UncompressedPoint
|
||||||
)
|
)
|
||||||
|
|
||||||
VAPID_PUBLIC_KEY = b64urlencode(raw_pub).decode('utf-8')
|
encoded_pub = b64urlencode(raw_pub)
|
||||||
VAPID_PRIVATE_KEY = VAPID_PRIVATE_PEM # pywebpush accepts the file path to the private PEM
|
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:
|
except Exception as e:
|
||||||
logger.error(f'Could not load or generate VAPID keys: {e}')
|
logger.error(f'Could not load or generate VAPID keys: {e}')
|
||||||
@@ -60,6 +64,9 @@ VAPID_SUBJECT = os.getenv('VAPID_SUBJECT', 'mailto:support@invario-software.de')
|
|||||||
FCM_API_KEY = os.getenv('FCM_API_KEY', '')
|
FCM_API_KEY = os.getenv('FCM_API_KEY', '')
|
||||||
|
|
||||||
|
|
||||||
|
def _get_vapid_public():
|
||||||
|
return VAPID_PUBLIC_KEY
|
||||||
|
|
||||||
def _get_username_hash(username):
|
def _get_username_hash(username):
|
||||||
"""Generates a deterministic hash for database lookups."""
|
"""Generates a deterministic hash for database lookups."""
|
||||||
if not username:
|
if not username:
|
||||||
@@ -115,29 +122,33 @@ def get_user_subscriptions(username):
|
|||||||
|
|
||||||
|
|
||||||
def save_push_subscription(username, subscription_obj):
|
def save_push_subscription(username, subscription_obj):
|
||||||
"""
|
import traceback # Hilft uns, Fehler genau zu sehen
|
||||||
Save a new push subscription for a user with field-level encryption.
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
|
print("--- DEBUG PUSH PAYLOAD ---")
|
||||||
|
print(subscription_obj)
|
||||||
|
|
||||||
endpoint = subscription_obj.get('endpoint')
|
endpoint = subscription_obj.get('endpoint')
|
||||||
if not endpoint:
|
keys = subscription_obj.get('keys', {})
|
||||||
logger.warning('Invalid subscription object: missing endpoint')
|
|
||||||
|
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
|
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']},
|
||||||
@@ -149,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),
|
||||||
@@ -165,39 +176,37 @@ 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
|
||||||
|
|
||||||
|
|
||||||
def remove_push_subscription(username, endpoint):
|
def remove_push_subscription(username, endpoint):
|
||||||
"""
|
|
||||||
Remove a push subscription by making it inactive.
|
|
||||||
"""
|
|
||||||
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]
|
||||||
subs_col = get_push_subscriptions_collection(db)
|
subs_col = get_push_subscriptions_collection(db)
|
||||||
|
|
||||||
# Recreate the deterministic hash to find the specific subscription
|
|
||||||
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},
|
||||||
{'$set': {'IsActive': False}}
|
{'$set': {'IsActive': False}}
|
||||||
)
|
)
|
||||||
|
|
||||||
client.close()
|
client.close()
|
||||||
return result.modified_count > 0
|
return result.matched_count > 0
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f'Error removing push subscription: {e}')
|
logger.error(f'Error removing push subscription: {e}')
|
||||||
return False
|
return False
|
||||||
@@ -305,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'],
|
||||||
@@ -316,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:
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -25,7 +25,7 @@ class PushNotificationManager {
|
|||||||
* Initialize push notification system
|
* Initialize push notification system
|
||||||
* Must be called after page load
|
* Must be called after page load
|
||||||
*/
|
*/
|
||||||
async init() {
|
async init(providedKey = null) {
|
||||||
if (!this.isSupported) {
|
if (!this.isSupported) {
|
||||||
console.log('Push notifications not supported in this browser');
|
console.log('Push notifications not supported in this browser');
|
||||||
return false;
|
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');
|
const keyResponse = await fetch('/api/push/vapid-key');
|
||||||
if (keyResponse.ok) {
|
if (keyResponse.ok) {
|
||||||
const keyData = await keyResponse.json();
|
const keyData = await keyResponse.json();
|
||||||
this.vapidKey = keyData.vapid_key;
|
// WICHTIG: Muss exakt mit dem Python-JSON-Key übereinstimmen!
|
||||||
|
this.vapidKey = keyData.publicKey;
|
||||||
} else {
|
} else {
|
||||||
console.warn('Failed to fetch VAPID key');
|
console.warn('Failed to fetch VAPID key');
|
||||||
return false;
|
return false;
|
||||||
@@ -154,20 +161,22 @@ 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 false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove subscription on server
|
|
||||||
const success = await this.removeSubscriptionFromServer(subscription);
|
|
||||||
|
|
||||||
// Unsubscribe from push service
|
|
||||||
if (success) {
|
|
||||||
await subscription.unsubscribe();
|
|
||||||
return true;
|
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) {
|
} catch (error) {
|
||||||
console.error('Failed to unsubscribe from push notifications:', error);
|
console.error('Failed to unsubscribe from push notifications:', error);
|
||||||
return false;
|
return false;
|
||||||
@@ -324,7 +333,7 @@ const pushNotificationManager = new PushNotificationManager();
|
|||||||
/**
|
/**
|
||||||
* Show notification subscription UI (typically in settings)
|
* Show notification subscription UI (typically in settings)
|
||||||
*/
|
*/
|
||||||
function showPushNotificationSettings() {
|
function showPushNotificationSettings(vapidPublicKey) {
|
||||||
const container = document.getElementById('push-notification-settings');
|
const container = document.getElementById('push-notification-settings');
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
@@ -350,9 +359,9 @@ function showPushNotificationSettings() {
|
|||||||
|
|
||||||
container.innerHTML = html;
|
container.innerHTML = html;
|
||||||
|
|
||||||
// Set up button handler
|
// Set up button handler and pass the VAPID public key to init()
|
||||||
const toggleBtn = document.getElementById('toggle-push-btn');
|
const toggleBtn = document.getElementById('toggle-push-btn');
|
||||||
pushNotificationManager.init().then(() => {
|
pushNotificationManager.init(vapidPublicKey).then(() => {
|
||||||
updatePushStatus();
|
updatePushStatus();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -89,7 +89,7 @@
|
|||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
if (typeof showPushNotificationSettings === 'function') {
|
if (typeof showPushNotificationSettings === 'function') {
|
||||||
showPushNotificationSettings();
|
showPushNotificationSettings('{{ vapid_public_key }}');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
+2
-1
@@ -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
|
||||||
Reference in New Issue
Block a user